task-91(店铺中心): 实现新增店铺表单
新增 shop-manage-form-model.ts(分组/店铺名/商城/账号/密码校验与 camel 创建 请求体,密码按敏感字段处理) 并在 shop-manage-api.ts 增加 submitCreateShopManage (POST /api/admin/shop-manages,createdById 由当前会话提供)。 TDD: task-91.test.ts 8 用例先 RED 后 GREEN。
This commit is contained in:
@@ -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 { 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 { parseShopManageGroups } from './shop-group-model.ts'
|
||||||
import {
|
import {
|
||||||
normalizeShopListParams,
|
normalizeShopListParams,
|
||||||
@@ -8,7 +9,13 @@ import {
|
|||||||
type ShopGroupOption,
|
type ShopGroupOption,
|
||||||
type ShopManageListParams,
|
type ShopManageListParams,
|
||||||
type ShopPageResult,
|
type ShopPageResult,
|
||||||
|
type ShopSummary,
|
||||||
} from './shop-dto.ts'
|
} from './shop-dto.ts'
|
||||||
|
import {
|
||||||
|
toShopManageCreateRequest,
|
||||||
|
validateShopManageForm,
|
||||||
|
type ShopManageFormValues,
|
||||||
|
} from './shop-manage-form-model.ts'
|
||||||
|
|
||||||
export const SHOP_MANAGE_ENDPOINT = '/api/admin/shop-manages'
|
export const SHOP_MANAGE_ENDPOINT = '/api/admin/shop-manages'
|
||||||
|
|
||||||
@@ -25,3 +32,17 @@ export async function fetchShopManageGroups(): Promise<ShopGroupOption[]> {
|
|||||||
const { data } = await http.get<unknown>(SHOP_MANAGE_GROUPS_ENDPOINT)
|
const { data } = await http.get<unknown>(SHOP_MANAGE_GROUPS_ENDPOINT)
|
||||||
return parseShopManageGroups(data)
|
return parseShopManageGroups(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 提交新增店铺:先校验(失败抛首条错误不发请求),POST /api/admin/shop-manages,解析返回记录。 */
|
||||||
|
export async function submitCreateShopManage(form: ShopManageFormValues, createdById: number): Promise<ShopSummary> {
|
||||||
|
const { valid, errors } = validateShopManageForm(form)
|
||||||
|
if (!valid) {
|
||||||
|
throw new Error(Object.values(errors)[0] || '店铺表单校验未通过')
|
||||||
|
}
|
||||||
|
const { data } = await http.post<unknown>(SHOP_MANAGE_ENDPOINT, toShopManageCreateRequest(form, createdById))
|
||||||
|
const item = toShopSummary(unwrap<unknown>(data))
|
||||||
|
if (!item) {
|
||||||
|
throw new Error('新增店铺响应异常:未返回有效记录')
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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/)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user