2026-03-26 16:46:22 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 群组列表数据源抽象:游标、偏移分页实现同一契约,GroupManager 只依赖接口。
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
|
GroupResponse,
|
|
|
|
|
|
GroupMemberResponse,
|
|
|
|
|
|
GroupListResponse,
|
|
|
|
|
|
GroupMemberListResponse,
|
|
|
|
|
|
CursorPaginationRequest,
|
|
|
|
|
|
} from '../types/dto';
|
|
|
|
|
|
import { groupService } from '../services/groupService';
|
2026-04-04 08:01:45 +08:00
|
|
|
|
import { groupCacheRepository } from '@/database';
|
2026-03-26 16:46:22 +08:00
|
|
|
|
|
|
|
|
|
|
export const GROUP_LIST_PAGE_SIZE = 20;
|
|
|
|
|
|
export const GROUP_MEMBER_LIST_PAGE_SIZE = 50;
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 群组列表数据源 ====================
|
|
|
|
|
|
|
|
|
|
|
|
/** 单次拉取结果(一页或一批) */
|
|
|
|
|
|
export interface GroupListPage {
|
|
|
|
|
|
items: GroupResponse[];
|
|
|
|
|
|
/** 在当前源上是否还能再 loadNext 一页 */
|
|
|
|
|
|
hasMore: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 分页式群组列表源:restart 后开始新序列;首次 loadNext 为第一页,之后为后续页。
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface IGroupListPagedSource {
|
|
|
|
|
|
restart(): void;
|
|
|
|
|
|
loadNext(): Promise<GroupListPage>;
|
|
|
|
|
|
/** 至少成功 loadNext 过一次且仍有下一页时为 true */
|
|
|
|
|
|
readonly hasMore: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 远端群组列表(游标分页)
|
|
|
|
|
|
*/
|
|
|
|
|
|
export class NetworkCursorGroupListPagedSource implements IGroupListPagedSource {
|
|
|
|
|
|
private nextCursor: string | null = null;
|
|
|
|
|
|
private hasMoreAfterLastLoad = false;
|
|
|
|
|
|
private loadedOnce = false;
|
|
|
|
|
|
private readonly pageSize: number;
|
|
|
|
|
|
|
|
|
|
|
|
constructor(pageSize: number = GROUP_LIST_PAGE_SIZE) {
|
|
|
|
|
|
this.pageSize = pageSize;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
restart(): void {
|
|
|
|
|
|
this.nextCursor = null;
|
|
|
|
|
|
this.hasMoreAfterLastLoad = false;
|
|
|
|
|
|
this.loadedOnce = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
get hasMore(): boolean {
|
|
|
|
|
|
return this.loadedOnce && this.hasMoreAfterLastLoad;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async loadNext(): Promise<GroupListPage> {
|
|
|
|
|
|
const params: CursorPaginationRequest = {
|
|
|
|
|
|
cursor: this.nextCursor ?? '',
|
|
|
|
|
|
page_size: this.pageSize,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const result = await groupService.getGroupsCursor(params);
|
|
|
|
|
|
|
|
|
|
|
|
this.hasMoreAfterLastLoad = result.has_more ?? false;
|
|
|
|
|
|
this.nextCursor = result.next_cursor ?? null;
|
|
|
|
|
|
this.loadedOnce = true;
|
|
|
|
|
|
|
|
|
|
|
|
return { items: result.list ?? [], hasMore: this.hasMoreAfterLastLoad };
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 远端群组列表(偏移分页)
|
|
|
|
|
|
*/
|
|
|
|
|
|
export class NetworkOffsetGroupListPagedSource implements IGroupListPagedSource {
|
|
|
|
|
|
private nextPage = 1;
|
|
|
|
|
|
private hasMoreAfterLastLoad = false;
|
|
|
|
|
|
private loadedOnce = false;
|
|
|
|
|
|
private readonly pageSize: number;
|
|
|
|
|
|
|
|
|
|
|
|
constructor(pageSize: number = GROUP_LIST_PAGE_SIZE) {
|
|
|
|
|
|
this.pageSize = pageSize;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
restart(): void {
|
|
|
|
|
|
this.nextPage = 1;
|
|
|
|
|
|
this.hasMoreAfterLastLoad = false;
|
|
|
|
|
|
this.loadedOnce = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
get hasMore(): boolean {
|
|
|
|
|
|
return this.loadedOnce && this.hasMoreAfterLastLoad;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async loadNext(): Promise<GroupListPage> {
|
|
|
|
|
|
const result = await groupService.getGroups(this.nextPage, this.pageSize);
|
|
|
|
|
|
|
|
|
|
|
|
const items = result.list ?? [];
|
|
|
|
|
|
const totalPages = result.total_pages ?? 1;
|
|
|
|
|
|
|
|
|
|
|
|
this.hasMoreAfterLastLoad = this.nextPage < totalPages;
|
|
|
|
|
|
this.nextPage++;
|
|
|
|
|
|
this.loadedOnce = true;
|
|
|
|
|
|
|
|
|
|
|
|
return { items, hasMore: this.hasMoreAfterLastLoad };
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* SQLite 群组列表缓存(单批,无后续页)
|
|
|
|
|
|
*/
|
|
|
|
|
|
export class SqliteGroupListPagedSource implements IGroupListPagedSource {
|
|
|
|
|
|
private consumed = false;
|
|
|
|
|
|
|
|
|
|
|
|
restart(): void {
|
|
|
|
|
|
this.consumed = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
get hasMore(): boolean {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async loadNext(): Promise<GroupListPage> {
|
|
|
|
|
|
if (this.consumed) {
|
|
|
|
|
|
return { items: [], hasMore: false };
|
|
|
|
|
|
}
|
|
|
|
|
|
this.consumed = true;
|
|
|
|
|
|
try {
|
2026-04-04 08:01:45 +08:00
|
|
|
|
const items = await groupCacheRepository.getAll();
|
2026-03-26 16:46:22 +08:00
|
|
|
|
return { items, hasMore: false };
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.warn('[SqliteGroupListPagedSource] 读取群组列表缓存失败:', e);
|
|
|
|
|
|
return { items: [], hasMore: false };
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 群组成员列表数据源 ====================
|
|
|
|
|
|
|
|
|
|
|
|
/** 成员列表单次拉取结果 */
|
|
|
|
|
|
export interface GroupMemberListPage {
|
|
|
|
|
|
items: GroupMemberResponse[];
|
|
|
|
|
|
hasMore: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 分页式群组成员列表源
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface IGroupMemberListPagedSource {
|
|
|
|
|
|
restart(): void;
|
|
|
|
|
|
loadNext(): Promise<GroupMemberListPage>;
|
|
|
|
|
|
readonly hasMore: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 远端群组成员列表(游标分页)
|
|
|
|
|
|
*/
|
|
|
|
|
|
export class NetworkCursorGroupMemberListPagedSource implements IGroupMemberListPagedSource {
|
|
|
|
|
|
private nextCursor: string | null = null;
|
|
|
|
|
|
private hasMoreAfterLastLoad = false;
|
|
|
|
|
|
private loadedOnce = false;
|
|
|
|
|
|
private readonly pageSize: number;
|
|
|
|
|
|
private readonly groupId: string;
|
|
|
|
|
|
|
|
|
|
|
|
constructor(groupId: string, pageSize: number = GROUP_MEMBER_LIST_PAGE_SIZE) {
|
|
|
|
|
|
this.groupId = groupId;
|
|
|
|
|
|
this.pageSize = pageSize;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
restart(): void {
|
|
|
|
|
|
this.nextCursor = null;
|
|
|
|
|
|
this.hasMoreAfterLastLoad = false;
|
|
|
|
|
|
this.loadedOnce = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
get hasMore(): boolean {
|
|
|
|
|
|
return this.loadedOnce && this.hasMoreAfterLastLoad;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async loadNext(): Promise<GroupMemberListPage> {
|
|
|
|
|
|
const params: CursorPaginationRequest = {
|
|
|
|
|
|
cursor: this.nextCursor ?? '',
|
|
|
|
|
|
page_size: this.pageSize,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const result = await groupService.getGroupMembersCursor(this.groupId, params);
|
|
|
|
|
|
|
|
|
|
|
|
this.hasMoreAfterLastLoad = result.has_more ?? false;
|
|
|
|
|
|
this.nextCursor = result.next_cursor ?? null;
|
|
|
|
|
|
this.loadedOnce = true;
|
|
|
|
|
|
|
|
|
|
|
|
return { items: result.list ?? [], hasMore: this.hasMoreAfterLastLoad };
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 远端群组成员列表(偏移分页)
|
|
|
|
|
|
*/
|
|
|
|
|
|
export class NetworkOffsetGroupMemberListPagedSource implements IGroupMemberListPagedSource {
|
|
|
|
|
|
private nextPage = 1;
|
|
|
|
|
|
private hasMoreAfterLastLoad = false;
|
|
|
|
|
|
private loadedOnce = false;
|
|
|
|
|
|
private readonly pageSize: number;
|
|
|
|
|
|
private readonly groupId: string;
|
|
|
|
|
|
|
|
|
|
|
|
constructor(groupId: string, pageSize: number = GROUP_MEMBER_LIST_PAGE_SIZE) {
|
|
|
|
|
|
this.groupId = groupId;
|
|
|
|
|
|
this.pageSize = pageSize;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
restart(): void {
|
|
|
|
|
|
this.nextPage = 1;
|
|
|
|
|
|
this.hasMoreAfterLastLoad = false;
|
|
|
|
|
|
this.loadedOnce = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
get hasMore(): boolean {
|
|
|
|
|
|
return this.loadedOnce && this.hasMoreAfterLastLoad;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async loadNext(): Promise<GroupMemberListPage> {
|
|
|
|
|
|
const result = await groupService.getMembers(
|
|
|
|
|
|
this.groupId,
|
|
|
|
|
|
this.nextPage,
|
|
|
|
|
|
this.pageSize
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const items = result.list ?? [];
|
|
|
|
|
|
const totalPages = result.total_pages ?? 1;
|
|
|
|
|
|
|
|
|
|
|
|
this.hasMoreAfterLastLoad = this.nextPage < totalPages;
|
|
|
|
|
|
this.nextPage++;
|
|
|
|
|
|
this.loadedOnce = true;
|
|
|
|
|
|
|
|
|
|
|
|
return { items, hasMore: this.hasMoreAfterLastLoad };
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* SQLite 群组成员列表缓存(单批,无后续页)
|
|
|
|
|
|
*/
|
|
|
|
|
|
export class SqliteGroupMemberListPagedSource implements IGroupMemberListPagedSource {
|
|
|
|
|
|
private consumed = false;
|
|
|
|
|
|
private readonly groupId: string;
|
|
|
|
|
|
|
|
|
|
|
|
constructor(groupId: string) {
|
|
|
|
|
|
this.groupId = groupId;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
restart(): void {
|
|
|
|
|
|
this.consumed = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
get hasMore(): boolean {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async loadNext(): Promise<GroupMemberListPage> {
|
|
|
|
|
|
if (this.consumed) {
|
|
|
|
|
|
return { items: [], hasMore: false };
|
|
|
|
|
|
}
|
|
|
|
|
|
this.consumed = true;
|
|
|
|
|
|
try {
|
2026-04-04 08:01:45 +08:00
|
|
|
|
const items = await groupCacheRepository.getMembers(this.groupId);
|
2026-03-26 16:46:22 +08:00
|
|
|
|
return { items, hasMore: false };
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.warn('[SqliteGroupMemberListPagedSource] 读取成员列表缓存失败:', e);
|
|
|
|
|
|
return { items: [], hasMore: false };
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 工厂函数 ====================
|
|
|
|
|
|
|
|
|
|
|
|
/** 远端群组列表分页策略 */
|
|
|
|
|
|
export type RemoteGroupListSourceKind = 'cursor' | 'offset';
|
|
|
|
|
|
|
|
|
|
|
|
export function createRemoteGroupListSource(
|
|
|
|
|
|
kind: RemoteGroupListSourceKind = 'cursor',
|
|
|
|
|
|
pageSize: number = GROUP_LIST_PAGE_SIZE
|
|
|
|
|
|
): IGroupListPagedSource {
|
|
|
|
|
|
if (kind === 'offset') {
|
|
|
|
|
|
return new NetworkOffsetGroupListPagedSource(pageSize);
|
|
|
|
|
|
}
|
|
|
|
|
|
return new NetworkCursorGroupListPagedSource(pageSize);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function createRemoteGroupMemberListSource(
|
|
|
|
|
|
groupId: string,
|
|
|
|
|
|
kind: RemoteGroupListSourceKind = 'cursor',
|
|
|
|
|
|
pageSize: number = GROUP_MEMBER_LIST_PAGE_SIZE
|
|
|
|
|
|
): IGroupMemberListPagedSource {
|
|
|
|
|
|
if (kind === 'offset') {
|
|
|
|
|
|
return new NetworkOffsetGroupMemberListPagedSource(groupId, pageSize);
|
|
|
|
|
|
}
|
|
|
|
|
|
return new NetworkCursorGroupMemberListPagedSource(groupId, pageSize);
|
|
|
|
|
|
}
|