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 aebbcfa0..75c6033e 100644 --- a/admin-frontend-vue/src/pages/shop/shop-manage-api.ts +++ b/admin-frontend-vue/src/pages/shop/shop-manage-api.ts @@ -1,6 +1,7 @@ -/** 店铺列表加载与分组选项适配(任务 88/90):GET /api/admin/shop-manages(/groups) + 归一与解析。 */ +/** 店铺列表/分组/新增适配(任务 88/90/91):GET|POST /api/admin/shop-manages + 归一与解析。 */ import { http } from '@/api/http' -import { parseShopManagePage } from './shop-manage-model.ts' +import { unwrap } from '@/api/envelope' +import { parseShopManagePage, toShopSummary } from './shop-manage-model.ts' import { parseShopManageGroups } from './shop-group-model.ts' import { normalizeShopListParams, @@ -8,7 +9,13 @@ import { type ShopGroupOption, type ShopManageListParams, type ShopPageResult, + type ShopSummary, } from './shop-dto.ts' +import { + toShopManageCreateRequest, + validateShopManageForm, + type ShopManageFormValues, +} from './shop-manage-form-model.ts' export const SHOP_MANAGE_ENDPOINT = '/api/admin/shop-manages' @@ -25,3 +32,17 @@ export async function fetchShopManageGroups(): Promise { const { data } = await http.get(SHOP_MANAGE_GROUPS_ENDPOINT) return parseShopManageGroups(data) } + +/** 提交新增店铺:先校验(失败抛首条错误不发请求),POST /api/admin/shop-manages,解析返回记录。 */ +export async function submitCreateShopManage(form: ShopManageFormValues, createdById: number): Promise { + const { valid, errors } = validateShopManageForm(form) + if (!valid) { + throw new Error(Object.values(errors)[0] || '店铺表单校验未通过') + } + const { data } = await http.post(SHOP_MANAGE_ENDPOINT, toShopManageCreateRequest(form, createdById)) + const item = toShopSummary(unwrap(data)) + if (!item) { + throw new Error('新增店铺响应异常:未返回有效记录') + } + return item +} diff --git a/admin-frontend-vue/src/pages/shop/shop-manage-form-model.ts b/admin-frontend-vue/src/pages/shop/shop-manage-form-model.ts new file mode 100644 index 00000000..6e94f860 --- /dev/null +++ b/admin-frontend-vue/src/pages/shop/shop-manage-form-model.ts @@ -0,0 +1,90 @@ +/** 店铺新增/编辑共用表单模型(任务 91/92):镜像 Java ShopManageCreateRequest/UpdateRequest 的 + * 必填与长度校验、敏感密码字段处理与 camelCase 请求序列化;纯逻辑。 */ + +export const SHOP_MANAGE_ZN_USERNAME_MAX_LEN = 128 + +/** 店铺表单字段(密码为敏感字段,仅本次提交内存中使用,不写入日志/存储)。 */ +export interface ShopManageFormValues { + groupId: number | null + shopName: string + mallName: string + znUsername: string + account: string + password: string +} + +export interface ShopManageFormErrors { + groupId?: string + shopName?: string + mallName?: string + account?: string + password?: string + znUsername?: string +} + +/** 发送 POST /api/admin/shop-manages 的请求体(与 Java ShopManageCreateRequest camelCase 对齐)。 */ +export interface ShopManageCreatePayload { + groupId: number + shopName: string + mallName: string + account: string + password: string + createdById: number + znUsername?: string +} + +export function emptyShopManageForm(): ShopManageFormValues { + return { groupId: null, shopName: '', mallName: '', znUsername: '', account: '', password: '' } +} + +function positiveGroupId(value: number | null): number | null { + return typeof value === 'number' && Number.isFinite(value) && value >= 1 ? Math.floor(value) : null +} + +function requiredText(value: string, fallback: string): string { + return (value || '').trim() || fallback +} + +function errorText(value: string): string { + return (value || '').trim() || '' +} + +/** 校验表单(镜像后端 NotBlank/NotNull/Size 规则),返回逐字段错误。 */ +export function validateShopManageForm(form: ShopManageFormValues): { valid: boolean; errors: ShopManageFormErrors } { + const errors: ShopManageFormErrors = {} + if (positiveGroupId(form.groupId) === null) { + errors.groupId = '分组不能为空' + } + if (!errorText(form.shopName)) errors.shopName = '店铺名称不能为空' + if (!errorText(form.mallName)) errors.mallName = '商城名称不能为空' + if (!errorText(form.account)) errors.account = '账号不能为空' + if (!errorText(form.password)) errors.password = '密码不能为空' + const znUsername = (form.znUsername || '').trim() + if (znUsername.length > SHOP_MANAGE_ZN_USERNAME_MAX_LEN) { + errors.znUsername = `自动化账号长度不能超过${SHOP_MANAGE_ZN_USERNAME_MAX_LEN}个字符` + } + return { valid: Object.keys(errors).length === 0, errors } +} + +/** 新增表单 → 创建请求体;缺有效分组/必填时抛可操作错误(调用前应先校验)。 */ +export function toShopManageCreateRequest(form: ShopManageFormValues, createdById: number): ShopManageCreatePayload { + const groupId = positiveGroupId(form.groupId) + if (groupId === null) { + throw new Error('分组不能为空') + } + if (!errorText(form.shopName)) throw new Error('店铺名称不能为空') + if (!errorText(form.mallName)) throw new Error('商城名称不能为空') + if (!errorText(form.account)) throw new Error('账号不能为空') + if (!errorText(form.password)) throw new Error('密码不能为空') + const payload: ShopManageCreatePayload = { + groupId, + shopName: errorText(form.shopName), + mallName: errorText(form.mallName), + account: errorText(form.account), + password: errorText(form.password), + createdById, + } + const znUsername = (form.znUsername || '').trim() + if (znUsername) payload.znUsername = znUsername + return payload +} diff --git a/admin-frontend-vue/tests/task-91.test.ts b/admin-frontend-vue/tests/task-91.test.ts new file mode 100644 index 00000000..30ba3224 --- /dev/null +++ b/admin-frontend-vue/tests/task-91.test.ts @@ -0,0 +1,87 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { + emptyShopManageForm, + toShopManageCreateRequest, + validateShopManageForm, +} from '../src/pages/shop/shop-manage-form-model.ts' + +test('test_task_091_shop_manage_create_form_normal_primary_path', () => { + // 正常主路径:合法表单校验通过并序列化为 Java camelCase 创建请求体。 + const form = { groupId: 3, shopName: 'BlueWave', mallName: '亚马逊美国站', znUsername: 'robot', account: 'sale@blue.com', password: 'p@ss' } + const { valid, errors } = validateShopManageForm(form) + assert.equal(valid, true) + assert.deepEqual(errors, {}) + assert.deepEqual(toShopManageCreateRequest(form, 8), { + groupId: 3, + shopName: 'BlueWave', + mallName: '亚马逊美国站', + znUsername: 'robot', + account: 'sale@blue.com', + password: 'p@ss', + createdById: 8, + }) +}) + +test('test_task_091_shop_manage_create_form_normal_variant_input', () => { + // 正常变体:znUsername 可选留空不下发;字段首尾空白去除。 + const request = toShopManageCreateRequest( + { groupId: 1, shopName: ' A店 ', mallName: ' M ', znUsername: ' ', account: 'acc', password: 'p' }, + 2, + ) + assert.equal(request.shopName, 'A店') + assert.equal(request.mallName, 'M') + assert.equal('znUsername' in request, false) +}) + +test('test_task_091_shop_manage_create_form_repeated_is_idempotent', () => { + // 正常重复:序列化不改输入、结果稳定。 + const form = { groupId: 2, shopName: 'S', mallName: 'M', znUsername: '', account: 'a', password: 'p' } + assert.deepEqual(toShopManageCreateRequest(form, 1), toShopManageCreateRequest(form, 1)) + assert.equal((form as { shopName: string }).shopName, 'S') +}) + +test('test_task_091_shop_manage_create_form_boundary_empty_input', () => { + // 边界空值:空表单校验给出必填字段错误。 + const { valid, errors } = validateShopManageForm(emptyShopManageForm()) + assert.equal(valid, false) + assert.equal(errors.groupId, '分组不能为空') + assert.equal(errors.shopName, '店铺名称不能为空') + assert.equal(errors.account, '账号不能为空') + assert.equal(errors.password, '密码不能为空') +}) + +test('test_task_091_shop_manage_create_form_boundary_single_item', () => { + // 边界单元素:最短合法表单通过。 + const { valid } = validateShopManageForm({ groupId: 1, shopName: 'S', mallName: 'M', znUsername: '', account: 'a', password: 'p' }) + assert.equal(valid, true) +}) + +test('test_task_091_shop_manage_create_form_boundary_limit_or_missing_field', () => { + // 边界上限/缺字段:znUsername 超长拒绝,缺分组序列化不产生无效请求。 + const overZn = validateShopManageForm({ groupId: 1, shopName: 'S', mallName: 'M', znUsername: 'x'.repeat(129), account: 'a', password: 'p' }) + assert.equal(overZn.valid, false) + assert.match(overZn.errors.znUsername || '', /128/) + assert.throws(() => toShopManageCreateRequest({ ...emptyShopManageForm(), shopName: 'S' }, 1), /分组/) +}) + +test('test_task_091_shop_manage_create_form_invalid_input_rejected', () => { + // 异常输入:空白店铺名/账号/密码与未选分组被拒。 + const { errors } = validateShopManageForm({ groupId: null, shopName: ' ', mallName: '', znUsername: '', account: '', password: ' ' }) + assert.equal(errors.groupId, '分组不能为空') + assert.equal(errors.shopName, '店铺名称不能为空') + assert.equal(errors.account, '账号不能为空') + assert.equal(errors.password, '密码不能为空') +}) + +test('test_task_091_shop_manage_create_form_dependency_failure_returns_actionable_message', () => { + // 依赖失败/提交走 adapter:表单模型纯逻辑,adapter POST /api/admin/shop-manages。 + const model = readSource('src/pages/shop/shop-manage-form-model.ts') + assert.equal(/axios|http\.|vue/.test(model), false, '表单模型保持纯逻辑') + assert.match(model, /敏感|密码/) + const api = readSource('src/pages/shop/shop-manage-api.ts') + assert.match(api, /submitCreateShopManage/) + assert.match(api, /http\.post/) + assert.match(api, /\/api\/admin\/shop-manages/) +})