perf(C6): 去重总数据列表顺序翻页改 keyset(前后端契约一起改)
背景:列表页 `ORDER BY id DESC LIMIT offset,size`,带筛选且选择性低时每页都要 对命中集做一次 filesort;深翻页 offset 也白扫索引。 改法(保持跳页/回退/改每页的原有行为): - 后端:page 接口新增可选 `last_id` 游标 —— 传了就 `id < lastId` + `LIMIT size`(无 offset), 响应新增 `nextLastId`(本页最后一行 id);不传仍是原 OFFSET 分页 - 管理前端:只有"下一页"用游标(上一页响应带回),跳页/改每页/筛选清空游标走 OFFSET - 测试:新增 2 个后端契约测试(keyset 无 offset + nextLastId;无游标保持 LIMIT 30,15) 验证:mvn test 2897 全绿;admin-frontend-vue vue-tsc 通过 + 1619 测试全绿; 已部署 JAR 2b9ab774c60c3f5d802c9a2cee549008(双节点 health=200)与 admin-vue-20260914-112055。
This commit is contained in:
@@ -43,6 +43,10 @@ const lockedGroupId = computed<number | null>(() => {
|
||||
return groups.value.length === 1 ? groups.value[0].id : null
|
||||
})
|
||||
const jumpPage = ref('')
|
||||
/** 顺序翻页游标:上一页响应给的 nextLastId;仅在"下一页"时使用,跳页/筛选时清空走 OFFSET。 */
|
||||
const pageCursor = ref<number | null>(null)
|
||||
/** 本次请求实际使用的游标(load 时决定) */
|
||||
let pendingCursor: number | null = null
|
||||
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
|
||||
|
||||
@@ -85,10 +89,15 @@ function stopExportWait(): void {
|
||||
async function load(): Promise<void> {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await fetchDedupeTotalList(toDedupeListParams(filter, page.value, pageSize.value))
|
||||
const result = await fetchDedupeTotalList(
|
||||
toDedupeListParams(filter, page.value, pageSize.value, pendingCursor),
|
||||
)
|
||||
rows.value = result.items
|
||||
total.value = result.total
|
||||
if (result.page >= 1) page.value = result.page
|
||||
// 本页响应回传的游标留作"下一页"用;空页则清空(没有更多)
|
||||
pageCursor.value = result.nextLastId ?? null
|
||||
pendingCursor = null
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '去重数据加载失败')
|
||||
} finally {
|
||||
@@ -98,11 +107,15 @@ async function load(): Promise<void> {
|
||||
|
||||
function apply(): void {
|
||||
page.value = 1
|
||||
pageCursor.value = null
|
||||
pendingCursor = null
|
||||
void load()
|
||||
}
|
||||
|
||||
function changePage(next: number): void {
|
||||
if (next < 1 || next > totalPages.value) return
|
||||
// 只有"顺序下一页"用 keyset 游标(避免深分页 offset);跳页/回退走 OFFSET,行为不变
|
||||
pendingCursor = next === page.value + 1 ? pageCursor.value : null
|
||||
page.value = next
|
||||
void load()
|
||||
}
|
||||
@@ -119,6 +132,8 @@ function goJump(): void {
|
||||
function changeSize(size: number) {
|
||||
pageSize.value = size
|
||||
page.value = 1
|
||||
pageCursor.value = null
|
||||
pendingCursor = null
|
||||
void load()
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ export interface AsinListParams {
|
||||
groupId?: number | null
|
||||
/** 国家代码(如 DE、UK)。 */
|
||||
country?: string
|
||||
/** 顺序翻页游标(上一页返回的 nextLastId):传了就忽略 page 偏移,走 keyset。 */
|
||||
lastId?: number
|
||||
}
|
||||
|
||||
/** 序列化到 Java 分页接口的查询参数(snake_case)。 */
|
||||
@@ -36,6 +38,8 @@ export interface AsinPageQuery {
|
||||
end_date?: string
|
||||
group_id?: number
|
||||
country?: string
|
||||
/** 顺序翻页游标(keyset):传了就忽略 page 偏移。 */
|
||||
last_id?: number
|
||||
}
|
||||
|
||||
function finiteInt(value: unknown): number | null {
|
||||
@@ -77,5 +81,6 @@ export function toAsinPageQuery(params: AsinListParams): AsinPageQuery {
|
||||
if (params.endDate) query.end_date = params.endDate
|
||||
if (params.groupId != null) query.group_id = params.groupId
|
||||
if (params.country) query.country = params.country
|
||||
if (typeof params.lastId === 'number' && params.lastId > 0) query.last_id = params.lastId
|
||||
return query
|
||||
}
|
||||
|
||||
@@ -30,8 +30,10 @@ export function toDedupeListParams(
|
||||
state: DedupeTotalFilterState,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
lastId?: number | null,
|
||||
): AsinListParams {
|
||||
const params: AsinListParams = { page, pageSize }
|
||||
if (typeof lastId === 'number' && lastId > 0) params.lastId = lastId
|
||||
const keyword = (state.keyword || '').trim()
|
||||
const username = (state.username || '').trim()
|
||||
const country = (state.country || '').trim()
|
||||
|
||||
@@ -18,6 +18,8 @@ export interface DedupeTotalPageResult {
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
/** 顺序翻页游标:本页最后一行 id;下一页回传它即可走 keyset。 */
|
||||
nextLastId?: number
|
||||
}
|
||||
|
||||
export function emptyDedupeTotalPage(): DedupeTotalPageResult {
|
||||
@@ -65,6 +67,8 @@ export function parseDedupeTotalPage(payload: unknown): DedupeTotalPageResult {
|
||||
}
|
||||
if (typeof record.total === 'number') out.total = Math.floor(record.total)
|
||||
if (typeof record.page === 'number' && record.page >= 1) out.page = Math.floor(record.page)
|
||||
const rawNextLastId = record.nextLastId ?? record.next_last_id
|
||||
if (typeof rawNextLastId === 'number' && rawNextLastId > 0) out.nextLastId = Math.floor(rawNextLastId)
|
||||
const rawSize = record.pageSize ?? record.page_size
|
||||
if (typeof rawSize === 'number' && rawSize >= 1) out.pageSize = Math.floor(rawSize)
|
||||
return out
|
||||
|
||||
Reference in New Issue
Block a user