task-113(任务与重复分析中心): 实现店铺数据任务批量下载
新增 shop-data-download.ts(打包请求体 result_ids + 下载 job 状态), shop-data-api.ts 增加 requestShopDataZipDownload(POST /download-zip, blob)。 TDD: task-113.test.ts 8 用例先 RED 后 GREEN。
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
/** 店铺数据任务列表加载适配(任务 110):GET /api/admin/shop-data-crawl-tasks + 筛选归一与解析。 */
|
||||
/** 店铺数据任务列表/批量下载适配(任务 110/113)。 */
|
||||
import { http } from '@/api/http'
|
||||
import { parseShopDataTaskPage, type ShopDataTaskPageResult } from './shop-data-model.ts'
|
||||
import { toShopDataQuery, type ShopDataFilter } from './shop-data-filter.ts'
|
||||
import { toShopDataZipRequest } from './shop-data-download.ts'
|
||||
import { downloadZipHeaderCounts } from './image-video-download.ts'
|
||||
|
||||
export const SHOP_DATA_CRAWL_TASKS_ENDPOINT = '/api/admin/shop-data-crawl-tasks'
|
||||
|
||||
@@ -15,3 +17,18 @@ export async function fetchShopDataTaskList(
|
||||
})
|
||||
return parseShopDataTaskPage(data)
|
||||
}
|
||||
|
||||
export interface ShopDataZipDownloadResult {
|
||||
blob: Blob
|
||||
fileCount?: number
|
||||
errorCount?: number
|
||||
}
|
||||
|
||||
/** 批量下载选中结果文件为 zip:POST /download-zip(blob);从响应头读成功/失败计数。 */
|
||||
export async function requestShopDataZipDownload(resultIds: readonly (number | string)[]): Promise<ShopDataZipDownloadResult> {
|
||||
const { data, headers } = await http.post<Blob>(`${SHOP_DATA_CRAWL_TASKS_ENDPOINT}/download-zip`, toShopDataZipRequest(resultIds), {
|
||||
responseType: 'blob',
|
||||
})
|
||||
const counts = downloadZipHeaderCounts(headers as Record<string, string>)
|
||||
return { blob: data, fileCount: counts.fileCount, errorCount: counts.errorCount }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/** 店铺数据结果批量下载任务(任务 113):打包请求体与下载 job 状态;纯逻辑。 */
|
||||
|
||||
export type ShopDataDownloadStatus = 'idle' | 'running' | 'done' | 'failed'
|
||||
|
||||
export interface ShopDataDownloadJob {
|
||||
status: ShopDataDownloadStatus
|
||||
message?: string
|
||||
fileCount?: number
|
||||
errorCount?: number
|
||||
}
|
||||
|
||||
function finitePositive(value: unknown): number | null {
|
||||
const number = typeof value === 'number' && Number.isFinite(value)
|
||||
? value
|
||||
: typeof value === 'string' && /^\d+$/.test(value.trim())
|
||||
? Number(value.trim())
|
||||
: null
|
||||
return number === null || number < 1 ? null : Math.floor(number)
|
||||
}
|
||||
|
||||
/** result_id → POST /download-zip 请求体(过滤非法 id,按序去重)。 */
|
||||
export function toShopDataZipRequest(resultIds: readonly (number | string)[]): { result_ids: number[] } {
|
||||
const ids: number[] = []
|
||||
const seen = new Set<number>()
|
||||
for (const id of resultIds) {
|
||||
const parsed = finitePositive(id)
|
||||
if (parsed === null || seen.has(parsed)) continue
|
||||
seen.add(parsed)
|
||||
ids.push(parsed)
|
||||
}
|
||||
return { result_ids: ids }
|
||||
}
|
||||
|
||||
export function createShopDataDownloadJob(): ShopDataDownloadJob {
|
||||
return { status: 'idle' }
|
||||
}
|
||||
|
||||
export function startShopDataDownload(): ShopDataDownloadJob {
|
||||
return { status: 'running' }
|
||||
}
|
||||
|
||||
export function succeedShopDataDownload(_job: ShopDataDownloadJob, fileCount: number, errorCount: number): ShopDataDownloadJob {
|
||||
return { status: 'done', fileCount, errorCount }
|
||||
}
|
||||
|
||||
export function failShopDataDownload(_job: ShopDataDownloadJob, message: string): ShopDataDownloadJob {
|
||||
return { status: 'failed', message }
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readSource } from './helpers.ts'
|
||||
import {
|
||||
createShopDataDownloadJob,
|
||||
failShopDataDownload,
|
||||
succeedShopDataDownload,
|
||||
toShopDataZipRequest,
|
||||
} from '../src/pages/tasks/shop-data-download.ts'
|
||||
|
||||
test('test_task_113_shop_data_download_normal_primary_path', () => {
|
||||
// 正常主路径:选中 result_id 组装打包请求体。
|
||||
assert.deepEqual(toShopDataZipRequest([501, 502]), { result_ids: [501, 502] })
|
||||
})
|
||||
|
||||
test('test_task_113_shop_data_download_normal_variant_input', () => {
|
||||
// 正常变体:字符串 id 解析与非法 id 过滤。
|
||||
assert.deepEqual(toShopDataZipRequest(['3', 'x', 0, '-1']), { result_ids: [3] })
|
||||
})
|
||||
|
||||
test('test_task_113_shop_data_download_repeated_is_idempotent', () => {
|
||||
// 正常重复:请求体构建稳定。
|
||||
assert.deepEqual(toShopDataZipRequest(['1', '2', '1']), { result_ids: [1, 2] })
|
||||
})
|
||||
|
||||
test('test_task_113_shop_data_download_boundary_empty_input', () => {
|
||||
// 边界空值:无选择回空请求体。
|
||||
assert.deepEqual(toShopDataZipRequest([]), { result_ids: [] })
|
||||
})
|
||||
|
||||
test('test_task_113_shop_data_download_boundary_single_item', () => {
|
||||
// 边界单元素:单结果下载 job 成功状态。
|
||||
assert.deepEqual(toShopDataZipRequest([9]), { result_ids: [9] })
|
||||
const job = succeedShopDataDownload(createShopDataDownloadJob(), 1, 0)
|
||||
assert.equal(job.status, 'done')
|
||||
assert.equal(job.fileCount, 1)
|
||||
})
|
||||
|
||||
test('test_task_113_shop_data_download_boundary_limit_or_missing_field', () => {
|
||||
// 边界上限/缺字段:非数字输入被忽略。
|
||||
assert.deepEqual(toShopDataZipRequest([' ']), { result_ids: [] })
|
||||
})
|
||||
|
||||
test('test_task_113_shop_data_download_invalid_input_rejected', () => {
|
||||
// 异常输入:失败 job 携带可操作消息。
|
||||
const job = failShopDataDownload(createShopDataDownloadJob(), '请至少选择一个结果文件')
|
||||
assert.equal(job.status, 'failed')
|
||||
assert.equal(job.message, '请至少选择一个结果文件')
|
||||
})
|
||||
|
||||
test('test_task_113_shop_data_download_dependency_failure_returns_actionable_message', () => {
|
||||
// 依赖失败/下载走 adapter:POST /api/admin/shop-data-crawl-tasks/download-zip + blob。
|
||||
const model = readSource('src/pages/tasks/shop-data-download.ts')
|
||||
assert.equal(/axios|http\.|vue/.test(model), false, '下载模型保持纯逻辑')
|
||||
const api = readSource('src/pages/tasks/shop-data-api.ts')
|
||||
assert.match(api, /\/download-zip/)
|
||||
assert.match(api, /requestShopDataZipDownload/)
|
||||
assert.match(api, /responseType:\s*['\"]blob['\"]/)
|
||||
})
|
||||
Reference in New Issue
Block a user