From 7fa9b5ccf3a6d293fa1f77b4f6dc3dc2fed7b0e7 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:48:26 +0800 Subject: [PATCH] =?UTF-8?q?task-93(=E5=BA=97=E9=93=BA=E4=B8=AD=E5=BF=83):?= =?UTF-8?q?=20=E5=AE=9E=E7=8E=B0=E5=BA=97=E9=93=BA=E5=87=AD=E8=AF=81?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E5=88=86=E7=A6=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 shop-credential-model.ts:凭证明文仅经凭证端点解码且落 SensitiveString, 与列表行(仅掩码)字段分离;shop-manage-api.ts 增加 fetchShopCredential (GET /api/admin/shop-manages/{id}/credential?shop_name=…)。 TDD: task-93.test.ts 8 用例先 RED 后 GREEN。 --- .../src/pages/shop/shop-credential-model.ts | 42 +++++++++++ .../src/pages/shop/shop-manage-api.ts | 14 +++- admin-frontend-vue/tests/task-93.test.ts | 72 +++++++++++++++++++ 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 admin-frontend-vue/src/pages/shop/shop-credential-model.ts create mode 100644 admin-frontend-vue/tests/task-93.test.ts diff --git a/admin-frontend-vue/src/pages/shop/shop-credential-model.ts b/admin-frontend-vue/src/pages/shop/shop-credential-model.ts new file mode 100644 index 00000000..328ae4e5 --- /dev/null +++ b/admin-frontend-vue/src/pages/shop/shop-credential-model.ts @@ -0,0 +1,42 @@ +/** 店铺明文凭据解析模型(任务 93):凭证明文仅来自凭证端点且落 SensitiveString, + * 与列表行(仅掩码)字段分离;纯逻辑。 */ +import { unwrap } from '../../api/envelope.ts' +import { toSensitiveString, type ShopCredential } 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 toShopCredential(raw: unknown): ShopCredential | null { + if (!raw || typeof raw !== 'object') return null + const record = raw as Record + const id = numberOrNull(record.id) + if (id === null) return null + const password = text(record.password) + if (!password) return null + const credential: ShopCredential = { + id, + groupId: numberOrNull(record.groupId ?? record.group_id), + groupName: text(record.groupName ?? record.group_name), + shopName: text(record.shopName ?? record.shop_name), + mallName: text(record.mallName ?? record.mall_name), + znUsername: text(record.znUsername ?? record.zn_username), + account: text(record.account), + password: toSensitiveString(password), + } + return credential +} + +/** 解包凭证端点负载(data 为 ShopManageCredentialVo)为明文凭据;无效时抛可操作错误。 */ +export function parseShopCredential(payload: unknown): ShopCredential { + const credential = toShopCredential(unwrap(payload)) + if (!credential) { + throw new Error('读取店铺凭证响应异常:未返回有效的明文凭据') + } + return credential +} 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 fe813d49..65c409e9 100644 --- a/admin-frontend-vue/src/pages/shop/shop-manage-api.ts +++ b/admin-frontend-vue/src/pages/shop/shop-manage-api.ts @@ -1,11 +1,13 @@ -/** 店铺列表/分组/新增适配(任务 88/90/91):GET|POST /api/admin/shop-manages + 归一与解析。 */ +/** 店铺列表/分组/新增/编辑/凭证适配(任务 88-93):/api/admin/shop-manages 系端点 + 归一与解析。 */ import { http } from '@/api/http' import { unwrap } from '@/api/envelope' import { parseShopManagePage, toShopSummary } from './shop-manage-model.ts' import { parseShopManageGroups } from './shop-group-model.ts' +import { toShopCredential } from './shop-credential-model.ts' import { normalizeShopListParams, toShopManagePageQuery, + type ShopCredential, type ShopGroupOption, type ShopManageListParams, type ShopPageResult, @@ -61,3 +63,13 @@ export async function submitUpdateShopManage(id: number, form: ShopManageFormVal } return item } + +/** 读取店铺明文凭据:GET /api/admin/shop-manages/{id}/credential?shop_name=…;明文仅在内存短时使用。 */ +export async function fetchShopCredential(id: number, shopName: string): Promise { + const { data } = await http.get(`${SHOP_MANAGE_ENDPOINT}/${id}/credential`, { params: { shop_name: shopName } }) + const credential = toShopCredential(unwrap(data)) + if (!credential) { + throw new Error('读取店铺凭证响应异常:未返回有效记录') + } + return credential +} diff --git a/admin-frontend-vue/tests/task-93.test.ts b/admin-frontend-vue/tests/task-93.test.ts new file mode 100644 index 00000000..f473b0cd --- /dev/null +++ b/admin-frontend-vue/tests/task-93.test.ts @@ -0,0 +1,72 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { toShopCredential } from '../src/pages/shop/shop-credential-model.ts' +import { toShopSummary } from '../src/pages/shop/shop-manage-model.ts' + +test('test_task_093_credential_separation_normal_primary_path', () => { + // 正常主路径:凭证明文只能经凭证解码产生,且落在 SensitiveString 字段。 + const credential = toShopCredential({ + id: 5, + groupId: 3, + groupName: '华东组', + shopName: 'BlueWave', + mallName: 'M', + znUsername: 'robot', + account: 'a@b.c', + password: 'plain-pw', + }) + assert.ok(credential) + assert.equal(credential.id, 5) + assert.equal(credential.shopName, 'BlueWave') + assert.equal(credential.account, 'a@b.c') + assert.equal(credential.password, 'plain-pw') +}) + +test('test_task_093_credential_separation_normal_variant_input', () => { + // 正常变体:凭证端点解码对象结构与列表行独立,不混入列表摘要。 + const row = toShopSummary({ id: 5, shopName: 'BlueWave', password: 'plain-pw', passwordMasked: '******' }) + assert.equal(row!.account, '') + assert.equal('password' in row!, false, '列表行不带密码明文字段') +}) + +test('test_task_093_credential_separation_repeated_is_idempotent', () => { + // 正常重复:凭证解码稳定、不改输入。 + const payload = { id: 1, shopName: 'A', account: 'x', password: 'p' } + assert.deepEqual(toShopCredential(payload), toShopCredential(payload)) +}) + +test('test_task_093_credential_separation_boundary_empty_input', () => { + // 边界空值:无密码凭证无法解码(避免误把空当明文)。 + assert.equal(toShopCredential({ id: 1, shopName: 'A', account: 'x', password: '' }), null) +}) + +test('test_task_093_credential_separation_boundary_single_item', () => { + // 边界单元素:单店铺凭证解码字段完整。 + const credential = toShopCredential({ id: 9, shopName: 'S', account: 'a', password: 'p', mallName: 'M' }) + assert.ok(credential) + assert.equal(credential.groupId, null) + assert.equal(credential.mallName, 'M') +}) + +test('test_task_093_credential_separation_boundary_limit_or_missing_field', () => { + // 边界上限/缺字段:凭证明文不为空时必须校验原样保留,缺 id 视为无效。 + assert.equal(toShopCredential('garbage'), null) + assert.equal(toShopCredential({ shopName: 'no id', password: 'p' }), null) +}) + +test('test_task_093_credential_separation_invalid_input_rejected', () => { + // 异常输入:无 id/无明文凭证解码为 null,明确拒绝进入通用对象。 + assert.equal(toShopCredential({ id: 1, shopName: 'A' }), null) + assert.equal(toShopCredential(null), null) +}) + +test('test_task_093_credential_separation_dependency_failure_returns_actionable_message', () => { + // 依赖失败/取凭证走 adapter:凭证模型纯逻辑;adapter GET /{id}/credential + SensitiveString。 + const model = readSource('src/pages/shop/shop-credential-model.ts') + assert.equal(/axios|http\.|vue/.test(model), false, '凭证模型保持纯逻辑') + assert.match(model, /SensitiveString/) + const api = readSource('src/pages/shop/shop-manage-api.ts') + assert.match(api, /fetchShopCredential/) + assert.match(api, /\/credential/) +})