From 5af4e1c23ef8f3e9ab3a5de721ba90f4b03544cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Thu, 10 Sep 2026 14:53:57 +0800 Subject: [PATCH] =?UTF-8?q?fix(admin-vue=20=E6=9F=A5=E8=AF=A2ASIN):=20?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E7=AD=9B=E9=80=89/=E5=88=86=E9=A1=B5?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E5=AF=B9=E9=BD=90=E5=90=8E=E7=AB=AF=20snake?= =?UTF-8?q?=5Fcase=EF=BC=8C=E5=AF=BC=E5=87=BA=20XLSX=20=E5=B8=A6=E4=B8=8A?= =?UTF-8?q?=E5=BD=93=E5=89=8D=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - query-asin-filter: toQueryAsinQuery 输出 page_size/group_id/shop_name(35bed42b 后端已改显式 snake, camel 参数被静默忽略导致分组/店铺名筛选无作用、pageSize 固定回落 15) - 新增 query-asin-export: 按当前筛选构建导出 URL,页面「导出 XLSX」不再恒为全量 - query-asin-import-api: 导入兜底分组 groupId→group_id(后端 importExcel 已改名,此前同样被丢弃) - query-asin-delete-import-api: 保持 groupId 并加注释(后端仍隐式 camel,统一需两端同步部署) - task-71 测试断言更新为 snake 契约(含 camel 不下发反断言)+ 新增导出 URL 用例 已上线 admin-vue-20260910-145200,线上 chunk 验证含 snake 参数;task-71 10/10 通过,vue-tsc 通过 --- .../src/pages/asin/QueryAsinPage.vue | 4 ++- .../asin/query-asin-delete-import-api.ts | 4 ++- .../src/pages/asin/query-asin-export.ts | 24 ++++++++++++++ .../src/pages/asin/query-asin-filter.ts | 17 +++++----- .../src/pages/asin/query-asin-import-api.ts | 5 +-- admin-frontend-vue/tests/task-71.test.ts | 32 ++++++++++++++----- 6 files changed, 66 insertions(+), 20 deletions(-) create mode 100644 admin-frontend-vue/src/pages/asin/query-asin-export.ts diff --git a/admin-frontend-vue/src/pages/asin/QueryAsinPage.vue b/admin-frontend-vue/src/pages/asin/QueryAsinPage.vue index 0c929943..6658bc3c 100644 --- a/admin-frontend-vue/src/pages/asin/QueryAsinPage.vue +++ b/admin-frontend-vue/src/pages/asin/QueryAsinPage.vue @@ -8,6 +8,7 @@ import { createQueryAsin, fetchQueryAsinList, fetchShopNamesByGroup } from './qu import { QUERY_ASIN_COUNTRIES, queryAsinDisplayRows, type QueryAsinItem } from './query-asin-model.ts' import { asinCountryLabel } from './asin-country.ts' import { createQueryAsinFilterState, type QueryAsinFilterState } from './query-asin-filter.ts' +import { toQueryAsinExportUrl } from './query-asin-export.ts' import { deleteQueryAsinCountryAsin, updateQueryAsinCountryAsin } from './query-asin-detail-api.ts' import { fetchQueryAsinImportProgress, startQueryAsinImport } from './query-asin-import-api.ts' import { fetchQueryAsinDeleteImportProgress, startQueryAsinDeleteImport } from './query-asin-delete-import-api.ts' @@ -263,8 +264,9 @@ async function submitImport(): Promise { // ---- 导出 ---- function doExport(): void { + // 带当前筛选(分组/店铺名/ASIN/国家)导出,范围与列表筛选一致(此前不带参数导致导出恒为全量)。 const anchor = document.createElement('a') - anchor.href = '/api/admin/query-asins/export' + anchor.href = toQueryAsinExportUrl(filter) document.body.appendChild(anchor) anchor.click() anchor.remove() diff --git a/admin-frontend-vue/src/pages/asin/query-asin-delete-import-api.ts b/admin-frontend-vue/src/pages/asin/query-asin-delete-import-api.ts index 3971903f..d7907d43 100644 --- a/admin-frontend-vue/src/pages/asin/query-asin-delete-import-api.ts +++ b/admin-frontend-vue/src/pages/asin/query-asin-delete-import-api.ts @@ -8,7 +8,9 @@ import { export const QUERY_ASIN_DELETE_IMPORT_ENDPOINT = '/api/admin/query-asins/delete-import' -/** 启动导入删除;groupId 为 Excel 未提供分组时的兜底,可选(>=1 才下发)。 */ +/** 启动导入删除;groupId 为 Excel 未提供分组时的兜底,可选(>=1 才下发)。 + * 注意:后端 deleteImportExcel 仍是隐式 camel 参数名(groupId),此处必须发 groupId; + * 与 import 的 group_id 不同源,后端统一为 snake 时需两端同步改。 */ export async function startQueryAsinDeleteImport(file: File, groupId?: number): Promise { const form = new FormData() form.append('file', file, file.name) diff --git a/admin-frontend-vue/src/pages/asin/query-asin-export.ts b/admin-frontend-vue/src/pages/asin/query-asin-export.ts new file mode 100644 index 00000000..43e28b21 --- /dev/null +++ b/admin-frontend-vue/src/pages/asin/query-asin-export.ts @@ -0,0 +1,24 @@ +/** 查询 ASIN 导出 URL 构建:按当前筛选下发 group_id/shop_name/asin/country(对齐 Java export @RequestParam snake_case)。 */ +import { normalizeQueryAsinCountry, type QueryAsinFilterState } from './query-asin-filter.ts' + +export const QUERY_ASIN_EXPORT_ENDPOINT = '/api/admin/query-asins/export' + +/** 按导出可用筛选(分组/店铺名/ASIN/国家)构建查询串;空白/非法条件不下发,避免导出范围被误解。 */ +export function buildQueryAsinExportQuery(state: QueryAsinFilterState): string { + const params = new URLSearchParams() + const groupId = state.groupId + if (typeof groupId === 'number' && groupId >= 1) params.set('group_id', String(Math.floor(groupId))) + const shopName = (state.shopName || '').trim() + if (shopName) params.set('shop_name', shopName) + const asin = (state.asin || '').trim() + if (asin) params.set('asin', asin) + const country = normalizeQueryAsinCountry(state.country) + if (country) params.set('country', country) + return params.toString() +} + +/** 导出下载地址:浏览器直接访问该 URL 触发流式下载;无筛选时为全量导出。 */ +export function toQueryAsinExportUrl(state: QueryAsinFilterState): string { + const query = buildQueryAsinExportQuery(state) + return query ? `${QUERY_ASIN_EXPORT_ENDPOINT}?${query}` : QUERY_ASIN_EXPORT_ENDPOINT +} diff --git a/admin-frontend-vue/src/pages/asin/query-asin-filter.ts b/admin-frontend-vue/src/pages/asin/query-asin-filter.ts index f2b3d2fb..f95bcc60 100644 --- a/admin-frontend-vue/src/pages/asin/query-asin-filter.ts +++ b/admin-frontend-vue/src/pages/asin/query-asin-filter.ts @@ -1,4 +1,5 @@ -/** 查询 ASIN 页面筛选/分页状态(任务 71):分组/店铺名/ASIN/国家,仅下发非空且受支持的条件;Java 参数名 camelCase。 */ +/** 查询 ASIN 页面筛选/分页状态(任务 71):分组/店铺名/ASIN/国家,仅下发非空且受支持的条件; + * Java 参数名 snake_case(与 asin-filter 共享契约一致):page_size/group_id/shop_name,裸 camel 后端收不到会导致筛选失效。 */ import { ASIN_PAGE_DEFAULT_SIZE } from './asin-filter.ts' import { QUERY_ASIN_COUNTRIES } from './query-asin-model.ts' @@ -24,9 +25,9 @@ export interface QueryAsinListParams { export interface QueryAsinQuery { page: number - pageSize: number - groupId?: number - shopName?: string + page_size: number + group_id?: number + shop_name?: string asin?: string country?: string } @@ -92,11 +93,11 @@ export function toQueryAsinParams( }) } -/** 前端查询参数 → Java 查询参数;与 QueryAsinController @RequestParam(camel) 对齐,仅下发非空字段。 */ +/** 前端查询参数 → Java 查询参数;与 QueryAsinController @RequestParam(snake_case: page_size/group_id/shop_name/asin/country) 对齐,仅下发非空字段。 */ export function toQueryAsinQuery(params: QueryAsinListParams): QueryAsinQuery { - const query: QueryAsinQuery = { page: params.page, pageSize: params.pageSize } - if (typeof params.groupId === 'number' && params.groupId >= 1) query.groupId = params.groupId - if (params.shopName) query.shopName = params.shopName + const query: QueryAsinQuery = { page: params.page, page_size: params.pageSize } + if (typeof params.groupId === 'number' && params.groupId >= 1) query.group_id = params.groupId + if (params.shopName) query.shop_name = params.shopName if (params.asin) query.asin = params.asin if (params.country) query.country = params.country return query diff --git a/admin-frontend-vue/src/pages/asin/query-asin-import-api.ts b/admin-frontend-vue/src/pages/asin/query-asin-import-api.ts index 643c8e78..b6c59f67 100644 --- a/admin-frontend-vue/src/pages/asin/query-asin-import-api.ts +++ b/admin-frontend-vue/src/pages/asin/query-asin-import-api.ts @@ -8,12 +8,13 @@ import { export const QUERY_ASIN_IMPORT_ENDPOINT = '/api/admin/query-asins/import' -/** 启动导入添加;groupId 为 Excel 未提供分组时的兜底,可选(>=1 才下发)。 */ +/** 启动导入添加;groupId 为 Excel 未提供分组时的兜底,可选(>=1 才下发)。 + * 参数名必须用 group_id:Java importExcel @RequestParam(name="group_id"),发 groupId 会被丢弃致兜底分组失效。 */ export async function startQueryAsinImport(file: File, groupId?: number): Promise { const form = new FormData() form.append('file', file, file.name) if (typeof groupId === 'number' && Number.isFinite(groupId) && groupId >= 1) { - form.append('groupId', String(Math.floor(groupId))) + form.append('group_id', String(Math.floor(groupId))) } const { data } = await http.post(QUERY_ASIN_IMPORT_ENDPOINT, form) return parseQueryAsinImportStart(data) diff --git a/admin-frontend-vue/tests/task-71.test.ts b/admin-frontend-vue/tests/task-71.test.ts index 429102e9..bb233326 100644 --- a/admin-frontend-vue/tests/task-71.test.ts +++ b/admin-frontend-vue/tests/task-71.test.ts @@ -15,6 +15,7 @@ import { toQueryAsinParams, toQueryAsinQuery, } from '../src/pages/asin/query-asin-filter.ts' +import { buildQueryAsinExportQuery, toQueryAsinExportUrl } from '../src/pages/asin/query-asin-export.ts' test('test_task_071_query_asin_list_normal_primary_path', () => { // 正常主路径:Java QueryAsinPageVo(camel) 宽行(每店铺 5 国家 ASIN)解析为前端分页结果。 @@ -115,24 +116,26 @@ test('test_task_071_query_asin_list_boundary_limit_or_missing_field', () => { 999, )) assert.equal(query.page, 1) - assert.equal(query.pageSize, 100) - assert.equal('groupId' in query, false, '分组 0 不应下发') - assert.equal('shopName' in query, false, '空白店铺名不应下发') + assert.equal(query.page_size, 100) + assert.equal('group_id' in query, false, '分组 0 不应下发') + assert.equal('shop_name' in query, false, '空白店铺名不应下发') assert.equal('asin' in query, false) assert.equal('country' in query, false, '不支持的国家码不应下发') - // 正常非空值仍以 Java camelCase 参数名下发。 + // 正常非空值以 Java snake_case 参数名下发(QueryAsinController @RequestParam(page_size/group_id/shop_name) 契约)。 const filled = toQueryAsinQuery(toQueryAsinParams( { groupId: 5, shopName: ' 蓝店 ', asin: 'B0X', country: 'de' }, 3, 25, )) assert.equal(filled.page, 3) - assert.equal(filled.pageSize, 25) - assert.equal(filled.groupId, 5) - assert.equal(filled.shopName, '蓝店') + assert.equal(filled.page_size, 25) + assert.equal(filled.group_id, 5) + assert.equal(filled.shop_name, '蓝店') assert.equal(filled.asin, 'B0X') assert.equal(filled.country, 'DE') - assert.equal('page_size' in (filled as Record), false, '不应下发 page_size(snake)') + assert.equal('pageSize' in (filled as Record), false, '不应下发 pageSize(camel):后端收不到会导致筛选/分页失效') + assert.equal('groupId' in (filled as Record), false, '不应下发 groupId(camel)') + assert.equal('shopName' in (filled as Record), false, '不应下发 shopName(camel)') }) test('test_task_071_query_asin_country_semantics', () => { @@ -174,3 +177,16 @@ test('test_task_071_query_asin_list_dependency_failure_returns_actionable_messag const model = readSource('src/pages/asin/query-asin-model.ts') assert.equal(/axios|http\./.test(model), false, '查询 ASIN 列表解析保持纯逻辑') }) + +test('test_task_071_query_asin_export_url_follows_filter', () => { + // 导出 URL 跟随当前筛选:非空条件以 snake 参数下发(后端 export @RequestParam 契约),空白/非法条件与无筛选时退回全量。 + const url = toQueryAsinExportUrl({ groupId: 5, shopName: ' 蓝店 ', asin: ' B0X ', country: 'de' }) + assert.equal(url.startsWith('/api/admin/query-asins/export?'), true) + const params = new URLSearchParams(url.split('?')[1]) + assert.equal(params.get('group_id'), '5') + assert.equal(params.get('shop_name'), '蓝店') + assert.equal(params.get('asin'), 'B0X') + assert.equal(params.get('country'), 'DE') + assert.equal(buildQueryAsinExportQuery({ groupId: 0, shopName: ' ', asin: '', country: 'XX' }), '', '空白/非法条件不下发') + assert.equal(toQueryAsinExportUrl({ groupId: null, shopName: '', asin: '', country: '' }), '/api/admin/query-asins/export', '无筛选即全量导出') +})