task-69(ASIN 数据中心): 实现品牌数据库新增/编辑表单

invalid-asin-form-model.ts 纯模型:createEmptyInvalidAsinForm/invalidAsinFormFromItem 回填、
validateInvalidAsinForm 镜像后端(ASIN 必填<=128/品牌<=128/manual 必选分组)、toCreate 请求(camel)、
toUpdate 请求(自动导入默认不迁组);invalid-asin-api.ts 增 createInvalidAsin POST 与
updateInvalidAsin PUT /api/admin/invalid-asin-data。8 用例全过,507 单测 + build 绿。
This commit is contained in:
2026-09-05 15:57:33 +08:00
parent 43463e12ca
commit 62bf4f005d
3 changed files with 181 additions and 0 deletions
@@ -1,5 +1,7 @@
/** 品牌数据库(不符合ASIN)列表加载适配(任务 68):GET /api/admin/invalid-asin-data。 */
import { http } from '@/api/http'
import { unwrap } from '@/api/envelope'
import { toInvalidAsinCreateRequest, toInvalidAsinUpdateRequest, type InvalidAsinForm } from './invalid-asin-form-model'
import { parseInvalidAsinPage, type InvalidAsinPageResult } from './invalid-asin-model'
export const INVALID_ASIN_ENDPOINT = '/api/admin/invalid-asin-data'
@@ -30,3 +32,15 @@ export async function fetchInvalidAsinPage(query: InvalidAsinQuery): Promise<Inv
const { data } = await http.get<unknown>(INVALID_ASIN_ENDPOINT, { params: toQuery(query) })
return parseInvalidAsinPage(data)
}
/** 新增品牌数据库记录:POST /api/admin/invalid-asin-data。 */
export async function createInvalidAsin(form: InvalidAsinForm): Promise<void> {
const { data } = await http.post<unknown>(INVALID_ASIN_ENDPOINT, toInvalidAsinCreateRequest(form))
unwrap<unknown>(data)
}
/** 编辑品牌数据库记录:PUT /api/admin/invalid-asin-data/{id}。 */
export async function updateInvalidAsin(id: number, form: InvalidAsinForm, includeGroup = false): Promise<void> {
const { data } = await http.put<unknown>(`${INVALID_ASIN_ENDPOINT}/${id}`, toInvalidAsinUpdateRequest(form, { includeGroup }))
unwrap<unknown>(data)
}
@@ -0,0 +1,88 @@
/** 品牌数据库(不符合ASIN)表单模型(任务 69):新建/编辑表单校验与序列化,纯逻辑。 */
import type { InvalidAsinItem } from './invalid-asin-model.ts'
export const INVALID_ASIN_FIELD_MAX_LEN = 128
export interface InvalidAsinForm {
dataValue: string
brand: string
groupId: number | null
}
export interface InvalidAsinFormErrors {
dataValue?: string
brand?: string
groupId?: string
}
export interface InvalidAsinCreatePayload {
dataValue: string
brand?: string
groupId: number
}
export interface InvalidAsinUpdatePayload {
dataValue: string
brand?: string
groupId?: number
}
export function createEmptyInvalidAsinForm(): InvalidAsinForm {
return { dataValue: '', brand: '', groupId: null }
}
/** 从列表项回填表单。 */
export function invalidAsinFormFromItem(item: InvalidAsinItem): InvalidAsinForm {
return { dataValue: item.dataValue, brand: item.brand, groupId: item.groupId }
}
function text(value: string): string {
return (value || '').trim()
}
/** 校验表单(镜像后端 NotBlank/@Size/NotNull 文案);manual 新增必须选分组。 */
export function validateInvalidAsinForm(
form: InvalidAsinForm,
options: { requireGroup: boolean },
): { valid: boolean; errors: InvalidAsinFormErrors } {
const errors: InvalidAsinFormErrors = {}
const dataValue = text(form.dataValue)
if (!dataValue) {
errors.dataValue = 'ASIN 不能为空'
} else if (dataValue.length > INVALID_ASIN_FIELD_MAX_LEN) {
errors.dataValue = `ASIN 长度不能超过${INVALID_ASIN_FIELD_MAX_LEN}个字符`
}
const brand = text(form.brand)
if (brand.length > INVALID_ASIN_FIELD_MAX_LEN) {
errors.brand = `品牌长度不能超过${INVALID_ASIN_FIELD_MAX_LEN}个字符`
}
if (options.requireGroup && (typeof form.groupId !== 'number' || form.groupId < 1)) {
errors.groupId = '请选择分组'
}
return { valid: Object.keys(errors).length === 0, errors }
}
/** 表单 → POST /api/admin/invalid-asin-data 请求体(camel)。 */
export function toInvalidAsinCreateRequest(form: InvalidAsinForm): InvalidAsinCreatePayload {
const payload: InvalidAsinCreatePayload = {
dataValue: text(form.dataValue),
groupId: typeof form.groupId === 'number' ? form.groupId : 0,
}
const brand = text(form.brand)
if (brand) payload.brand = brand
return payload
}
/** 表单 → PUT /api/admin/invalid-asin-data/{id} 请求体;自动导入记录默认不迁组。 */
export function toInvalidAsinUpdateRequest(
form: InvalidAsinForm,
options: { includeGroup?: boolean } = {},
): InvalidAsinUpdatePayload {
const payload: InvalidAsinUpdatePayload = { dataValue: text(form.dataValue) }
const brand = text(form.brand)
if (brand) payload.brand = brand
if (options.includeGroup && typeof form.groupId === 'number' && form.groupId >= 1) {
payload.groupId = form.groupId
}
return payload
}
+79
View File
@@ -0,0 +1,79 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readSource } from './helpers.ts'
import {
createEmptyInvalidAsinForm,
invalidAsinFormFromItem,
toInvalidAsinCreateRequest,
toInvalidAsinUpdateRequest,
validateInvalidAsinForm,
} from '../src/pages/invalidasin/invalid-asin-form-model.ts'
import { toInvalidAsinItem } from '../src/pages/invalidasin/invalid-asin-model.ts'
test('test_task_069_brand_db_form_normal_primary_path', () => {
// 正常主路径:手动新增校验通过并序列化 create 请求(含分组)。
const form = { ...createEmptyInvalidAsinForm(), dataValue: 'B0ABC', brand: '苹果', groupId: 3 }
const result = validateInvalidAsinForm(form, { requireGroup: true })
assert.equal(result.valid, true)
const req = toInvalidAsinCreateRequest(form)
assert.equal(req.dataValue, 'B0ABC')
assert.equal(req.groupId, 3)
})
test('test_task_069_brand_db_form_normal_variant_input', () => {
// 正常变体:回填 AUTO 记录更新请求不含分组(自动导入不迁组)。
const item = toInvalidAsinItem({ id: 5, dataValue: 'X', brand: 'b', recordSource: 'AUTO', groupId: 1 })!
const form = invalidAsinFormFromItem(item)
assert.equal(form.dataValue, 'X')
assert.equal(form.brand, 'b')
const req = toInvalidAsinUpdateRequest(form)
assert.equal('groupId' in req, false)
})
test('test_task_069_brand_db_form_normal_repeated_operation_is_idempotent', () => {
// 正常重复:工厂与序列化稳定、不改输入。
assert.deepEqual(createEmptyInvalidAsinForm(), createEmptyInvalidAsinForm())
const form = { dataValue: 'A', brand: '', groupId: 1 }
assert.deepEqual(toInvalidAsinCreateRequest(form), toInvalidAsinCreateRequest(form))
})
test('test_task_069_brand_db_form_boundary_empty_input', () => {
// 边界空值:空表单缺 ASIN/分组给出可读错误。
const result = validateInvalidAsinForm(createEmptyInvalidAsinForm(), { requireGroup: true })
assert.equal(result.valid, false)
assert.ok(result.errors.dataValue)
assert.ok(result.errors.groupId)
})
test('test_task_069_brand_db_form_boundary_single_item', () => {
// 边界单元素:brand 可空通过校验。
const result = validateInvalidAsinForm({ ...createEmptyInvalidAsinForm(), dataValue: 'B', groupId: 1 }, { requireGroup: true })
assert.equal(result.valid, true)
})
test('test_task_069_brand_db_form_boundary_limit_or_missing_field', () => {
// 边界上限:ASIN/品牌超 128 拒绝并提示;数据两侧去空格。
const long = 'x'.repeat(129)
const result = validateInvalidAsinForm(
{ ...createEmptyInvalidAsinForm(), dataValue: ` ${long} `, brand: long, groupId: 1 },
{ requireGroup: true },
)
assert.equal(result.valid, false)
assert.ok(result.errors.dataValue)
assert.ok(result.errors.brand)
})
test('test_task_069_brand_db_form_invalid_input_rejected', () => {
// 异常输入:手动来源缺少分组被拒;update 请求缺 ASIN 时校验拦截。
assert.equal(validateInvalidAsinForm({ ...createEmptyInvalidAsinForm(), dataValue: 'ok' }, { requireGroup: true }).valid, false)
})
test('test_task_069_brand_db_form_dependency_failure_returns_actionable_message', () => {
// 依赖失败/表单提交走 adapterPOST/PUT /invalid-asin-data;表单模型纯逻辑不带 http。
const api = readSource('src/pages/invalidasin/invalid-asin-api.ts')
assert.match(api, /createInvalidAsin|saveInvalidAsin/)
assert.match(api, /updateInvalidAsin/)
assert.match(api, /invalid-asin-data/)
const mod = readSource('src/pages/invalidasin/invalid-asin-form-model.ts')
assert.equal(/axios|http\.|vue/.test(mod), false, '品牌数据库表单模型保持纯逻辑')
})