From b60bf7bc130c833bec27390ab4f992b75432fe47 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 17:35:34 +0800 Subject: [PATCH] =?UTF-8?q?task-128(=E8=AE=B0=E5=BD=95=E4=B8=8E=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E4=B8=AD=E5=BF=83):=20=E5=AE=9E=E7=8E=B0=E8=BD=AF?= =?UTF-8?q?=E4=BB=B6=E7=89=88=E6=9C=AC=E4=B8=8A=E4=BC=A0=E8=A1=A8=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 version-upload-model.ts(版本号/文件校验与表单字段) 与 uploadSoftwareVersion (multipart POST /api/admin/version)。 TDD: task-128.test.ts 8 用例 RED→GREEN。 --- .../src/pages/records/version-api.ts | 9 +++ .../src/pages/records/version-upload-model.ts | 27 ++++++++ admin-frontend-vue/tests/task-128.test.ts | 64 +++++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 admin-frontend-vue/src/pages/records/version-upload-model.ts create mode 100644 admin-frontend-vue/tests/task-128.test.ts diff --git a/admin-frontend-vue/src/pages/records/version-api.ts b/admin-frontend-vue/src/pages/records/version-api.ts index a992fe08..2200c131 100644 --- a/admin-frontend-vue/src/pages/records/version-api.ts +++ b/admin-frontend-vue/src/pages/records/version-api.ts @@ -4,8 +4,17 @@ import { parseSoftwareVersionList } from './version-model.ts' import type { SoftwareVersionList } from './version-dto.ts' export const SOFTWARE_VERSIONS_ENDPOINT = '/api/admin/versions' +export const SOFTWARE_VERSION_UPLOAD_ENDPOINT = '/api/admin/version' export async function fetchSoftwareVersions(): Promise { const { data } = await http.get(SOFTWARE_VERSIONS_ENDPOINT) return parseSoftwareVersionList(data) } + +/** 上传客户端软件版本(multipart version + file)。 */ +export async function uploadSoftwareVersion(version: string, file: Blob, filename = ''): Promise { + const form = new FormData() + form.append('version', version) + form.append('file', file, filename || (file instanceof File ? file.name : 'version.zip')) + await http.post(SOFTWARE_VERSION_UPLOAD_ENDPOINT, form) +} diff --git a/admin-frontend-vue/src/pages/records/version-upload-model.ts b/admin-frontend-vue/src/pages/records/version-upload-model.ts new file mode 100644 index 00000000..93748289 --- /dev/null +++ b/admin-frontend-vue/src/pages/records/version-upload-model.ts @@ -0,0 +1,27 @@ +/** 软件版本上传表单(任务 128):版本号/文件校验与 multipart 表单字段构建;纯逻辑。 */ + +export interface VersionUploadForm { + version: string + file: { name?: string; size?: number } | null +} + +export interface VersionUploadErrors { + version?: string + file?: string +} + +/** 校验上传表单(版本号必填非空、文件已选且非空);返回逐字段错误。 */ +export function validateSoftwareVersionUpload(form: VersionUploadForm): { valid: boolean; errors: VersionUploadErrors } { + const errors: VersionUploadErrors = {} + const version = (form.version || '').trim() + if (!version) errors.version = '版本号不能为空' + const file = form.file + if (!file) errors.file = '请选择安装包文件' + else if (typeof file.size === 'number' && file.size <= 0) errors.file = '安装包为空或无效' + return { valid: Object.keys(errors).length === 0, errors } +} + +/** 版本号 → multipart 表单字段(空白去除);文件在 adapter 端追加。 */ +export function buildSoftwareVersionUploadRequest(form: VersionUploadForm): { version: string } { + return { version: (form.version || '').trim() } +} diff --git a/admin-frontend-vue/tests/task-128.test.ts b/admin-frontend-vue/tests/task-128.test.ts new file mode 100644 index 00000000..05d27ff9 --- /dev/null +++ b/admin-frontend-vue/tests/task-128.test.ts @@ -0,0 +1,64 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { + buildSoftwareVersionUploadRequest, + validateSoftwareVersionUpload, +} from '../src/pages/records/version-upload-model.ts' + +test('test_task_128_version_upload_normal_primary_path', () => { + // 正常主路径:合法版本+文件校验通过并序列化 multipart 表单字段。 + const form = { version: '1.0.46', file: { name: 'a.zip', size: 1024 } } + const { valid, errors } = validateSoftwareVersionUpload(form) + assert.equal(valid, true) + assert.deepEqual(errors, {}) + assert.deepEqual(buildSoftwareVersionUploadRequest(form), { version: '1.0.46' }) +}) + +test('test_task_128_version_upload_normal_variant_input', () => { + // 正常变体:版本首尾空白去除。 + const request = buildSoftwareVersionUploadRequest({ version: ' 1.0.46 ', file: { name: 'b.zip', size: 1 } }) + assert.equal(request.version, '1.0.46') +}) + +test('test_task_128_version_upload_repeated_is_idempotent', () => { + // 正常重复:序列化稳定、不改输入。 + const form = { version: '1.0.0', file: { name: 'a.zip', size: 5 } } + assert.deepEqual(buildSoftwareVersionUploadRequest(form), buildSoftwareVersionUploadRequest(form)) + assert.equal((form as { version: string }).version, '1.0.0') +}) + +test('test_task_128_version_upload_boundary_empty_input', () => { + // 边界空值:空版本/无文件校验失败。 + const { errors } = validateSoftwareVersionUpload({ version: '', file: null }) + assert.equal(errors.version, '版本号不能为空') + assert.equal(errors.file, '请选择安装包文件') +}) + +test('test_task_128_version_upload_boundary_single_item', () => { + // 边界单元素:仅版本号合法但缺文件报错。 + const { errors } = validateSoftwareVersionUpload({ version: '1.0.0', file: null }) + assert.equal(errors.file, '请选择安装包文件') +}) + +test('test_task_128_version_upload_boundary_limit_or_missing_field', () => { + // 边界上限/缺字段:空文件按未选择处理。 + const { errors } = validateSoftwareVersionUpload({ version: '1.0.0', file: { name: 'x.zip', size: 0 } }) + assert.equal(errors.file, '安装包为空或无效') +}) + +test('test_task_128_version_upload_invalid_input_rejected', () => { + // 异常输入:非法版本字符仅去空白,不做语义强校验。 + const request = buildSoftwareVersionUploadRequest({ version: ' v1 ', file: { name: 'x.zip', size: 1 } }) + assert.equal(request.version, 'v1') +}) + +test('test_task_128_version_upload_dependency_failure_returns_actionable_message', () => { + // 依赖失败/提交走 adapter:上传表单纯逻辑;POST /api/admin/version multipart。 + const mod = readSource('src/pages/records/version-upload-model.ts') + assert.equal(/axios|http\.|vue/.test(mod), false, '上传表单保持纯逻辑') + const api = readSource('src/pages/records/version-api.ts') + assert.match(api, /uploadSoftwareVersion/) + assert.match(api, /\/api\/admin\/version/) + assert.match(api, /FormData/) +})