From 72a8f75dc5d3f074cf033c9b68e0e5b75708c3ac 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:38:02 +0800 Subject: [PATCH] =?UTF-8?q?task-84(=E5=BA=97=E9=93=BA=E4=B8=AD=E5=BF=83):?= =?UTF-8?q?=20=E5=AE=9E=E7=8E=B0=E6=96=B0=E5=A2=9E=E5=BA=97=E9=93=BA?= =?UTF-8?q?=E5=AF=86=E9=92=A5=E8=A1=A8=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 shop-key-form-model.ts(备注/紫鸟账号/令牌校验与 camel 请求体序列化, 镜像 Java NotBlank/Size 规则,令牌去 Bearer 前缀) 并在 shop-key-api.ts 增加 submitCreateShopKey(POST /api/admin/shop-keys)。 TDD: task-84.test.ts 8 用例先 RED 后 GREEN。 --- .../src/pages/shop/shop-key-api.ts | 32 ++++++- .../src/pages/shop/shop-key-form-model.ts | 76 +++++++++++++++ admin-frontend-vue/tests/task-84.test.ts | 92 +++++++++++++++++++ 3 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 admin-frontend-vue/src/pages/shop/shop-key-form-model.ts create mode 100644 admin-frontend-vue/tests/task-84.test.ts diff --git a/admin-frontend-vue/src/pages/shop/shop-key-api.ts b/admin-frontend-vue/src/pages/shop/shop-key-api.ts index 975df67d..38c68d8d 100644 --- a/admin-frontend-vue/src/pages/shop/shop-key-api.ts +++ b/admin-frontend-vue/src/pages/shop/shop-key-api.ts @@ -1,7 +1,19 @@ -/** 店铺密钥列表加载适配(任务 82):GET /api/admin/shop-keys + 分页归一与解析。 */ +/** 店铺密钥列表加载与新增适配(任务 82/84):GET/POST /api/admin/shop-keys + 归一与解析。 */ import { http } from '@/api/http' -import { parseShopKeyPage } from './shop-key-model' -import { normalizeKeyListParams, toKeyPageQuery, type ShopKeyListParams, type ShopKeyPageResult } from './shop-dto.ts' +import { unwrap } from '@/api/envelope' +import { parseShopKeyPage, toShopKeyItem } from './shop-key-model.ts' +import { + normalizeKeyListParams, + toKeyPageQuery, + type ShopKeyItem, + type ShopKeyListParams, + type ShopKeyPageResult, +} from './shop-dto.ts' +import { + toShopKeyCreateRequest, + validateShopKeyForm, + type ShopKeyFormValues, +} from './shop-key-form-model.ts' export const SHOP_KEYS_ENDPOINT = '/api/admin/shop-keys' @@ -10,3 +22,17 @@ export async function fetchShopKeyList(params: Partial = {}): const { data } = await http.get(SHOP_KEYS_ENDPOINT, { params: toKeyPageQuery(normalized) }) return parseShopKeyPage(data) } + +/** 提交新增店铺密钥:先校验(失败抛首条错误不发请求),再 POST,解析返回的新记录。 */ +export async function submitCreateShopKey(form: ShopKeyFormValues): Promise { + const { valid, errors } = validateShopKeyForm(form) + if (!valid) { + throw new Error(Object.values(errors)[0] || '店铺密钥表单校验未通过') + } + const { data } = await http.post(SHOP_KEYS_ENDPOINT, toShopKeyCreateRequest(form)) + const item = toShopKeyItem(unwrap(data)) + if (!item) { + throw new Error('新增店铺密钥响应异常:未返回有效记录') + } + return item +} diff --git a/admin-frontend-vue/src/pages/shop/shop-key-form-model.ts b/admin-frontend-vue/src/pages/shop/shop-key-form-model.ts new file mode 100644 index 00000000..ebc668a5 --- /dev/null +++ b/admin-frontend-vue/src/pages/shop/shop-key-form-model.ts @@ -0,0 +1,76 @@ +/** 店铺密钥表单模型(任务 84/85 共用):新增/编辑都镜像 Java ShopKeyCreateRequest/UpdateRequest 的 + * 字段、长度校验与敏感字段语义;纯逻辑,无框架依赖。 */ + +export const SHOP_KEY_REMARK_MAX_LEN = 128 +export const SHOP_KEY_ACCOUNT_MAX_LEN = 128 +export const SHOP_KEY_TOKEN_MAX_LEN = 512 + +export interface ShopKeyFormValues { + remarkName: string + ziniaoAccountName: string + /** 紫鸟令牌:敏感输入,提交前需去除 "Bearer " 前缀与首尾空白。 */ + ziniaoToken: string +} + +export interface ShopKeyFormErrors { + remarkName?: string + ziniaoAccountName?: string + ziniaoToken?: string +} + +/** 发送 POST/PUT /api/admin/shop-keys 的请求体(与 Java 请求 DTO camelCase 对齐)。 */ +export interface ShopKeyPayload { + remarkName?: string + ziniaoAccountName: string + ziniaoToken: string +} + +export function emptyShopKeyForm(): ShopKeyFormValues { + return { remarkName: '', ziniaoAccountName: '', ziniaoToken: '' } +} + +/** 归一紫鸟令牌:去首尾空白并去除大小写不敏感的 "Bearer " 前缀(镜像后端 normalizeToken)。 */ +export function normalizeZiniaoToken(value: string): string { + let token = (value || '').trim() + if (token.toUpperCase().startsWith('BEARER ')) { + token = token.slice(7).trim() + } + return token +} + +function remarkTrimmed(value: string): string { + return (value || '').trim() +} + +/** 校验新增/编辑表单(镜像后端 NotBlank/Size 规则),返回逐字段错误。 */ +export function validateShopKeyForm(form: ShopKeyFormValues): { valid: boolean; errors: ShopKeyFormErrors } { + const errors: ShopKeyFormErrors = {} + const remark = remarkTrimmed(form.remarkName) + if (remark.length > SHOP_KEY_REMARK_MAX_LEN) { + errors.remarkName = `备注名长度不能超过${SHOP_KEY_REMARK_MAX_LEN}个字符` + } + const account = (form.ziniaoAccountName || '').trim() + if (!account) { + errors.ziniaoAccountName = '紫鸟账号名称不能为空' + } else if (account.length > SHOP_KEY_ACCOUNT_MAX_LEN) { + errors.ziniaoAccountName = `紫鸟账号名称长度不能超过${SHOP_KEY_ACCOUNT_MAX_LEN}个字符` + } + const token = normalizeZiniaoToken(form.ziniaoToken) + if (!token) { + errors.ziniaoToken = '紫鸟令牌不能为空' + } else if (token.length > SHOP_KEY_TOKEN_MAX_LEN) { + errors.ziniaoToken = `紫鸟令牌长度不能超过${SHOP_KEY_TOKEN_MAX_LEN}个字符` + } + return { valid: Object.keys(errors).length === 0, errors } +} + +/** 新增表单 → 创建请求体:账号/令牌归一,备注为空则不下发。 */ +export function toShopKeyCreateRequest(form: ShopKeyFormValues): ShopKeyPayload { + const remark = remarkTrimmed(form.remarkName) + const payload: ShopKeyPayload = { + ziniaoAccountName: (form.ziniaoAccountName || '').trim(), + ziniaoToken: normalizeZiniaoToken(form.ziniaoToken), + } + if (remark) payload.remarkName = remark + return payload +} diff --git a/admin-frontend-vue/tests/task-84.test.ts b/admin-frontend-vue/tests/task-84.test.ts new file mode 100644 index 00000000..757dc145 --- /dev/null +++ b/admin-frontend-vue/tests/task-84.test.ts @@ -0,0 +1,92 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { + SHOP_KEY_ACCOUNT_MAX_LEN, + SHOP_KEY_REMARK_MAX_LEN, + SHOP_KEY_TOKEN_MAX_LEN, + emptyShopKeyForm, + toShopKeyCreateRequest, + validateShopKeyForm, +} from '../src/pages/shop/shop-key-form-model.ts' + +test('test_task_084_shop_key_create_form_normal_primary_path', () => { + // 正常主路径:合法表单校验通过并序列化为 Java camelCase 请求体。 + const form = emptyShopKeyForm() + form.remarkName = '主店' + form.ziniaoAccountName = 'blue_agent' + form.ziniaoToken = 'token-secret-1' + const { valid, errors } = validateShopKeyForm(form) + assert.equal(valid, true) + assert.deepEqual(errors, {}) + assert.deepEqual(toShopKeyCreateRequest(form), { + remarkName: '主店', + ziniaoAccountName: 'blue_agent', + ziniaoToken: 'token-secret-1', + }) +}) + +test('test_task_084_shop_key_create_form_normal_variant_input', () => { + // 正常变体:可选备注留空不下发;令牌首尾空白去除。 + const request = toShopKeyCreateRequest({ + remarkName: ' ', + ziniaoAccountName: ' red_agent ', + ziniaoToken: ' tok-2 ', + }) + assert.equal('remarkName' in request, false) + assert.equal(request.ziniaoAccountName, 'red_agent') + assert.equal(request.ziniaoToken, 'tok-2') +}) + +test('test_task_084_shop_key_create_form_repeated_is_idempotent', () => { + // 正常重复:序列化不改输入、结果稳定。 + const form = { remarkName: 'A', ziniaoAccountName: 'acc', ziniaoToken: 'tok' } + assert.deepEqual(toShopKeyCreateRequest(form), toShopKeyCreateRequest(form)) + assert.deepEqual(form, { remarkName: 'A', ziniaoAccountName: 'acc', ziniaoToken: 'tok' }) +}) + +test('test_task_084_shop_key_create_form_boundary_empty_input', () => { + // 边界空值:空表单校验给出账号/令牌缺失错误,备注可选不报错。 + const { valid, errors } = validateShopKeyForm(emptyShopKeyForm()) + assert.equal(valid, false) + assert.equal(errors.ziniaoAccountName, '紫鸟账号名称不能为空') + assert.equal(errors.ziniaoToken, '紫鸟令牌不能为空') + assert.equal('remarkName' in errors, false) +}) + +test('test_task_084_shop_key_create_form_boundary_single_item', () => { + // 边界单元素:仅账号+令牌的最短合法表单通过。 + const { valid } = validateShopKeyForm({ remarkName: '', ziniaoAccountName: 'a', ziniaoToken: 't' }) + assert.equal(valid, true) +}) + +test('test_task_084_shop_key_create_form_boundary_limit_or_missing_field', () => { + // 边界上限:各字段长度上限校验与超限拒绝。 + const long = 'x'.repeat(SHOP_KEY_REMARK_MAX_LEN + 1) + const account = 'a'.repeat(SHOP_KEY_ACCOUNT_MAX_LEN) + const token = 't'.repeat(SHOP_KEY_TOKEN_MAX_LEN) + const ok = validateShopKeyForm({ remarkName: 'x'.repeat(SHOP_KEY_REMARK_MAX_LEN), ziniaoAccountName: account, ziniaoToken: token }) + assert.equal(ok.valid, true) + const over = validateShopKeyForm({ remarkName: long, ziniaoAccountName: account, ziniaoToken: token }) + assert.equal(over.valid, false) + assert.match(over.errors.remarkName || '', /128/) +}) + +test('test_task_084_shop_key_create_form_invalid_input_rejected', () => { + // 异常输入:空白令牌/账号拒绝;超长备注给出可操作消息。 + const { errors } = validateShopKeyForm({ remarkName: '', ziniaoAccountName: ' ', ziniaoToken: ' ' }) + assert.equal(errors.ziniaoAccountName, '紫鸟账号名称不能为空') + assert.equal(errors.ziniaoToken, '紫鸟令牌不能为空') +}) + +test('test_task_084_shop_key_create_form_dependency_failure_returns_actionable_message', () => { + // 依赖失败/提交走 adapter:模型纯逻辑,adapter POST /api/admin/shop-keys + 校验。 + const model = readSource('src/pages/shop/shop-key-form-model.ts') + assert.equal(/axios|http\.|vue/.test(model), false, '表单模型保持纯逻辑') + assert.match(model, /SensitiveString|敏感/) + const api = readSource('src/pages/shop/shop-key-api.ts') + assert.match(api, /submitCreateShopKey/) + assert.match(api, /http\.post/) + assert.match(api, /\/api\/admin\/shop-keys/) + assert.match(api, /validateShopKeyForm/) +})