refactor: restructure core architecture and responsive system
All checks were successful
Frontend CI / ota-android (push) Successful in 1m40s
Frontend CI / ota-ios (push) Successful in 1m39s
Frontend CI / build-and-push-web (push) Successful in 3m30s
Frontend CI / build-android-apk (push) Successful in 1h1m8s

This commit implements a major architectural refactor to improve modularity, type safety, and maintainability across the codebase.

Key changes include:
- **Responsive System Refactor**: Migrated from a monolithic `useResponsive` hook to a set of specialized, granular hooks (`useBreakpoint`, `useOrientation`, `usePlatform`, etc.) located in `src/presentation/hooks/responsive`. This reduces unnecessary re-renders and improves developer experience.
- **Core Service Restructuring**: Introduced a new directory structure for services, including dedicated folders for `datasources`, `mappers`, and domain-specific types (`message`, `post`).
- **Data Layer Improvements**:
    - Centralized JSON parsing logic in `src/database/core/jsonUtils.ts`.
    - Cleaned up repository implementations by removing redundant local utility functions.
    - Refactored `MessageMapper` to use the new centralized JSON utility.
- **Type System Cleanup**:
    - Decomposed the large `src/types/dto.ts` into a modular `src/types/dto/` directory.
    - Simplified `src/types/index.ts` and introduced backward-compatible aliases for core entities.
- **Utility Consolidation**:
    - Created a centralized `src/utils/formatTime.ts` to replace fragmented date formatting logic across various screens and components.
    - Removed deprecated responsive utility files in favor of the new hook-based system.
- **Service Logic Refinement**: Refactored `ApiClient` and `WebSocketService` to use a centralized `showVerificationModal` service, removing duplicated state management for verification prompts.
This commit is contained in:
2026-05-05 19:07:33 +08:00
parent f5f9c3a619
commit 3196972596
96 changed files with 4609 additions and 3149 deletions

View File

@@ -3,12 +3,11 @@
* 处理用户身份认证申请、状态查询等功能
*/
import { api } from '../core/api';
import { api, PaginatedData } from '../core/api';
import {
VerificationStatusDTO,
VerificationRecordDTO,
SubmitVerificationRequest,
PaginatedData,
} from '@/types/dto';
// 提交认证响应

View File

@@ -10,6 +10,7 @@ import Constants from 'expo-constants';
import { Platform } from 'react-native';
import { eventBus } from '@/core/events/EventBus';
import { showVerificationModal } from './verification';
// 生产地址 https://withyou.littlelan.cn
const getBaseUrl = () => {
@@ -73,8 +74,7 @@ interface JwtPayload {
// API 客户端类
class ApiClient {
private baseUrl: string;
private verificationModalShown = false;
private refreshLock: Promise<boolean> | null = null;
private refreshLock: Promise<boolean> | null = null;
private isAuthFailed = false; // 全局认证失败标志,防止无限重试
constructor(baseUrl: string) {
@@ -82,12 +82,7 @@ class ApiClient {
}
private handleVerificationRequired() {
if (this.verificationModalShown) return;
this.verificationModalShown = true;
eventBus.emit({ type: 'SHOW_VERIFICATION_MODAL' });
setTimeout(() => {
this.verificationModalShown = false;
}, 3000);
showVerificationModal();
}
private navigateToLogin() {

View File

@@ -0,0 +1,98 @@
/**
* API 数据源实现
* 封装所有 API 调用,统一错误处理和请求拦截
*/
import { api, ApiResponse, ApiError } from '@/services/core';
import { IApiDataSource, DataSourceError } from './interfaces';
export class ApiDataSource implements IApiDataSource {
private baseUrl: string;
constructor(baseUrl?: string) {
this.baseUrl = baseUrl || '';
}
private buildUrl(url: string): string {
if (url.startsWith('http')) {
return url;
}
return `${this.baseUrl}${url}`;
}
private handleError(error: unknown, operation: string): never {
if (error instanceof ApiError) {
throw new DataSourceError(
error.message,
String(error.code),
'ApiDataSource',
error
);
}
const message = error instanceof Error ? error.message : 'Unknown error';
throw new DataSourceError(
`API ${operation} failed: ${message}`,
'API_ERROR',
'ApiDataSource',
error instanceof Error ? error : undefined
);
}
async get<T>(url: string, params?: any): Promise<T> {
try {
const fullUrl = this.buildUrl(url);
const response = await api.get<T>(fullUrl, params);
return response.data;
} catch (error) {
this.handleError(error, 'GET');
}
}
async post<T>(url: string, data?: any): Promise<T> {
try {
const fullUrl = this.buildUrl(url);
const response = await api.post<T>(fullUrl, data);
return response.data;
} catch (error) {
this.handleError(error, 'POST');
}
}
async put<T>(url: string, data?: any): Promise<T> {
try {
const fullUrl = this.buildUrl(url);
const response = await api.put<T>(fullUrl, data);
return response.data;
} catch (error) {
this.handleError(error, 'PUT');
}
}
async delete<T>(url: string, data?: any): Promise<T> {
try {
const fullUrl = this.buildUrl(url);
const response = await api.delete<T>(fullUrl, data);
return response.data;
} catch (error) {
this.handleError(error, 'DELETE');
}
}
async upload<T>(
url: string,
file: { uri: string; name: string; type: string },
additionalData?: Record<string, string>
): Promise<T> {
try {
const fullUrl = this.buildUrl(url);
const response = await api.upload<T>(fullUrl, file, additionalData);
return response.data;
} catch (error) {
this.handleError(error, 'UPLOAD');
}
}
}
// 导出单例实例
export const apiDataSource = new ApiDataSource();

View File

@@ -0,0 +1,199 @@
/**
* 缓存数据源实现
* 基于内存和 AsyncStorage 的混合缓存实现
*/
import AsyncStorage from '@react-native-async-storage/async-storage';
import { ICacheDataSource, DataSourceError } from './interfaces';
interface CacheEntry<T> {
value: T;
expiry: number | null; // null 表示永不过期
}
export class CacheDataSource implements ICacheDataSource {
private memoryCache: Map<string, CacheEntry<any>> = new Map();
private maxSize: number;
private defaultTtl: number; // 默认过期时间(毫秒)
private persistPrefix: string;
constructor(config?: {
maxSize?: number;
defaultTtl?: number; // 默认5分钟
persistPrefix?: string;
}) {
this.maxSize = config?.maxSize || 100;
this.defaultTtl = config?.defaultTtl || 5 * 60 * 1000;
this.persistPrefix = config?.persistPrefix || 'cache_';
}
/**
* 检查 key 是否过期
*/
private isExpired(entry: CacheEntry<any>): boolean {
if (entry.expiry === null) return false;
return Date.now() > entry.expiry;
}
/**
* 清理过期缓存
*/
private cleanup(): void {
const now = Date.now();
for (const [key, entry] of this.memoryCache.entries()) {
if (entry.expiry !== null && now > entry.expiry) {
this.memoryCache.delete(key);
}
}
// 如果仍然超过最大大小,删除最旧的条目
while (this.memoryCache.size > this.maxSize) {
const firstKey = this.memoryCache.keys().next().value;
if (firstKey !== undefined) {
this.memoryCache.delete(firstKey);
}
}
}
/**
* 生成持久化存储 key
*/
private getPersistKey(key: string): string {
return `${this.persistPrefix}${key}`;
}
// ==================== ICacheDataSource 实现 ====================
async get<T>(key: string): Promise<T | null> {
try {
// 先检查内存缓存
const memoryEntry = this.memoryCache.get(key);
if (memoryEntry) {
if (!this.isExpired(memoryEntry)) {
return memoryEntry.value as T;
}
// 过期了,从内存中删除
this.memoryCache.delete(key);
}
// 尝试从持久化存储读取
const persistKey = this.getPersistKey(key);
const stored = await AsyncStorage.getItem(persistKey);
if (stored) {
const entry: CacheEntry<T> = JSON.parse(stored);
if (!this.isExpired(entry)) {
// 重新加载到内存缓存
this.memoryCache.set(key, entry);
this.cleanup();
return entry.value;
}
// 过期了,删除
await AsyncStorage.removeItem(persistKey);
}
return null;
} catch (error) {
console.warn(`[CacheDataSource] Failed to get ${key}:`, error);
return null;
}
}
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
try {
const expiry = ttl === undefined
? (this.defaultTtl > 0 ? Date.now() + this.defaultTtl : null)
: (ttl > 0 ? Date.now() + ttl : null);
const entry: CacheEntry<T> = { value, expiry };
// 存入内存
this.memoryCache.set(key, entry);
this.cleanup();
// 持久化存储(重要数据)
if (ttl === undefined || ttl > 0) {
const persistKey = this.getPersistKey(key);
await AsyncStorage.setItem(persistKey, JSON.stringify(entry));
}
} catch (error) {
throw new DataSourceError(
`Failed to set cache ${key}`,
'CACHE_SET_ERROR',
'CacheDataSource',
error instanceof Error ? error : undefined
);
}
}
async delete(key: string): Promise<void> {
try {
// 从内存删除
this.memoryCache.delete(key);
// 从持久化存储删除
const persistKey = this.getPersistKey(key);
await AsyncStorage.removeItem(persistKey);
} catch (error) {
throw new DataSourceError(
`Failed to delete cache ${key}`,
'CACHE_DELETE_ERROR',
'CacheDataSource',
error instanceof Error ? error : undefined
);
}
}
async clear(): Promise<void> {
try {
// 清空内存
this.memoryCache.clear();
// 清空持久化存储中的所有缓存项
const keys = await AsyncStorage.getAllKeys();
const cacheKeys = keys.filter(k => k.startsWith(this.persistPrefix));
if (cacheKeys.length > 0) {
await AsyncStorage.multiRemove(cacheKeys);
}
} catch (error) {
throw new DataSourceError(
'Failed to clear cache',
'CACHE_CLEAR_ERROR',
'CacheDataSource',
error instanceof Error ? error : undefined
);
}
}
async has(key: string): Promise<boolean> {
try {
// 检查内存
const memoryEntry = this.memoryCache.get(key);
if (memoryEntry && !this.isExpired(memoryEntry)) {
return true;
}
// 检查持久化存储
const persistKey = this.getPersistKey(key);
const stored = await AsyncStorage.getItem(persistKey);
if (stored) {
const entry: CacheEntry<any> = JSON.parse(stored);
return !this.isExpired(entry);
}
return false;
} catch (error) {
return false;
}
}
async getMultiple<T>(keys: string[]): Promise<(T | null)[]> {
return Promise.all(keys.map(key => this.get<T>(key)));
}
async setMultiple<T>(entries: { key: string; value: T; ttl?: number }[]): Promise<void> {
await Promise.all(entries.map(entry => this.set(entry.key, entry.value, entry.ttl)));
}
}
// 导出单例实例
export const cacheDataSource = new CacheDataSource();

View File

@@ -0,0 +1,184 @@
/**
* WSClient - WebSocket连接管理
* 只负责WebSocket连接管理提供事件驱动架构
*/
import {
wsService,
WSChatMessage,
WSGroupChatMessage,
WSReadMessage,
WSGroupReadMessage,
WSRecallMessage,
WSGroupRecallMessage,
WSGroupTypingMessage,
WSGroupNoticeMessage,
WSMessageType,
} from '@/services/core';
// 事件处理器类型
type EventHandler<T> = (data: T) => void;
// WS事件类型
export interface WSEvents {
'chat': WSChatMessage;
'group_message': WSGroupChatMessage;
'read': WSReadMessage;
'group_read': WSGroupReadMessage;
'recall': WSRecallMessage;
'group_recall': WSGroupRecallMessage;
'group_typing': WSGroupTypingMessage;
'group_notice': WSGroupNoticeMessage;
'connected': void;
'disconnected': void;
'error': Error;
}
export type WSEventType = keyof WSEvents;
class WSClient {
private handlers: Map<WSEventType, Set<EventHandler<any>>> = new Map();
private unsubscribeFns: Array<() => void> = [];
private isInitialized = false;
/**
* 初始化WebSocket监听
*/
initialize(): void {
if (this.isInitialized) return;
// 监听私聊消息
const unsubChat = wsService.on('chat', (message) => {
this.emit('chat', message);
});
// 监听群聊消息
const unsubGroupMessage = wsService.on('group_message', (message) => {
this.emit('group_message', message);
});
// 监听私聊已读回执
const unsubRead = wsService.on('read', (message) => {
this.emit('read', message);
});
// 监听群聊已读回执
const unsubGroupRead = wsService.on('group_read', (message) => {
this.emit('group_read', message);
});
// 监听私聊消息撤回
const unsubRecall = wsService.on('recall', (message) => {
this.emit('recall', message);
});
// 监听群聊消息撤回
const unsubGroupRecall = wsService.on('group_recall', (message) => {
this.emit('group_recall', message);
});
// 监听群聊输入状态
const unsubGroupTyping = wsService.on('group_typing', (message) => {
this.emit('group_typing', message);
});
// 监听群通知
const unsubGroupNotice = wsService.on('group_notice', (message) => {
this.emit('group_notice', message);
});
// 监听连接状态
const unsubConnect = wsService.onConnect(() => {
this.emit('connected', undefined);
});
const unsubDisconnect = wsService.onDisconnect(() => {
this.emit('disconnected', undefined);
});
this.unsubscribeFns = [
unsubChat,
unsubGroupMessage,
unsubRead,
unsubGroupRead,
unsubRecall,
unsubGroupRecall,
unsubGroupTyping,
unsubGroupNotice,
unsubConnect,
unsubDisconnect,
];
this.isInitialized = true;
}
/**
* 销毁WebSocket监听
*/
destroy(): void {
this.unsubscribeFns.forEach((fn) => fn());
this.unsubscribeFns = [];
this.handlers.clear();
this.isInitialized = false;
}
/**
* 订阅事件
*/
on<T extends WSEventType>(
event: T,
handler: EventHandler<WSEvents[T]>
): () => void {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event)!.add(handler);
return () => {
this.handlers.get(event)?.delete(handler);
};
}
/**
* 触发事件
*/
private emit<T extends WSEventType>(
event: T,
data: WSEvents[T]
): void {
const handlers = this.handlers.get(event);
if (handlers) {
handlers.forEach((handler) => {
try {
handler(data);
} catch (error) {
console.error(`[WSClient] 事件处理器执行失败: ${event}`, error);
}
});
}
}
/**
* 检查连接状态
*/
isConnected(): boolean {
return wsService.isConnected();
}
/**
* 启动WebSocket连接
*/
async connect(): Promise<boolean> {
return wsService.start();
}
/**
* 断开WebSocket连接
*/
disconnect(): void {
wsService.stop();
}
}
export const wsClient = new WSClient();
export default wsClient;

View File

@@ -0,0 +1,9 @@
/**
* 数据源导出
* 统一导出所有数据源实现
*/
export * from './interfaces';
export * from './ApiDataSource';
export { LocalDataSource, localDataSource } from '@/database';
export * from './CacheDataSource';

View File

@@ -0,0 +1,68 @@
/**
* 数据源接口定义
* 定义所有数据源的标准接口,便于替换和测试
*/
// API 数据源接口
export interface IApiDataSource {
get<T>(url: string, params?: any): Promise<T>;
post<T>(url: string, data?: any): Promise<T>;
put<T>(url: string, data?: any): Promise<T>;
delete<T>(url: string, data?: any): Promise<T>;
upload<T>(url: string, file: { uri: string; name: string; type: string }, additionalData?: Record<string, string>): Promise<T>;
}
// 本地数据源接口 (SQLite)
export interface ILocalDataSource {
query<T>(sql: string, params?: any[]): Promise<T[]>;
getFirst<T>(sql: string, params?: any[]): Promise<T | null>;
execute(sql: string, params?: any[]): Promise<void>;
run(sql: string, params?: any[]): Promise<{ lastInsertRowId: number; changes: number }>;
transaction(operations: () => Promise<void>): Promise<void>;
batch(operations: (db: ILocalDataSource) => Promise<void>): Promise<void>;
enqueueWrite<T>(operation: () => Promise<T>): Promise<T>;
initialize(userId?: string): Promise<void>;
switchDatabase(userId?: string | null): Promise<void>;
closeDatabase(): Promise<void>;
}
// 缓存数据源接口
export interface ICacheDataSource {
get<T>(key: string): Promise<T | null>;
set<T>(key: string, value: T, ttl?: number): Promise<void>;
delete(key: string): Promise<void>;
clear(): Promise<void>;
has(key: string): Promise<boolean>;
getMultiple<T>(keys: string[]): Promise<(T | null)[]>;
setMultiple<T>(entries: { key: string; value: T; ttl?: number }[]): Promise<void>;
}
// 数据源错误类型
export class DataSourceError extends Error {
constructor(
message: string,
public code: string,
public source: string,
public originalError?: Error
) {
super(message);
this.name = 'DataSourceError';
}
}
// 数据源配置
export interface DataSourceConfig {
api?: {
baseUrl: string;
timeout?: number;
retryCount?: number;
};
local?: {
dbName: string;
version: number;
};
cache?: {
maxSize: number;
defaultTtl: number;
};
}

View File

@@ -0,0 +1,12 @@
import { eventBus } from '@/core/events/EventBus';
let verificationModalShown = false;
export function showVerificationModal(): void {
if (verificationModalShown) return;
verificationModalShown = true;
eventBus.emit({ type: 'SHOW_VERIFICATION_MODAL' });
setTimeout(() => {
verificationModalShown = false;
}, 3000);
}

View File

@@ -4,6 +4,7 @@ import { api, WS_URL } from './api';
import { eventBus } from '@/core/events/EventBus';
import { MessageCategory, SystemMessageType, SystemMessageExtraData, MessageSegment } from '@/types/dto';
import { systemNotificationService } from '../notification/systemNotificationService';
import { showVerificationModal } from './verification';
export type WSMessageType =
| 'chat'
@@ -1147,15 +1148,8 @@ class WebSocketService {
this.lastActivityAt = Date.now();
}
private verificationModalShown = false;
private handleVerificationRequired(): void {
if (this.verificationModalShown) return;
this.verificationModalShown = true;
eventBus.emit({ type: 'SHOW_VERIFICATION_MODAL' });
setTimeout(() => {
this.verificationModalShown = false;
}, 3000);
showVerificationModal();
}
private startHeartbeat(): void {

View File

@@ -0,0 +1,302 @@
/**
* 会话数据映射器
* 负责 Conversation 模型与 API 响应、数据库记录之间的转换
*/
import {
ConversationModel,
ConversationType,
UserModel,
GroupModel,
MessageModel,
} from '../models';
import type {
ConversationResponse,
ConversationDetailResponse,
UserDTO,
GroupResponse,
MessageResponse,
} from '../../types/dto';
import { MessageMapper } from './MessageMapper';
// 数据库会话记录类型
export interface ConversationDbRecord {
id: string;
participantId: string;
lastMessageId: string | null;
lastSeq: number;
unreadCount: number;
createdAt: string;
updatedAt: string;
}
// 缓存数据类型
export interface ConversationCacheData {
id: string;
type: string;
is_pinned?: boolean;
last_seq?: number;
last_message?: any;
last_message_at?: string;
unread_count?: number;
participants?: any[];
my_last_read_seq?: number;
other_last_read_seq?: number;
group?: any;
created_at?: string;
updated_at?: string;
}
export class ConversationMapper {
/**
* 将 API 列表响应转换为应用模型
*/
static fromApiResponse(response: ConversationResponse): ConversationModel {
return {
id: String(response.id || ''),
type: (response.type as ConversationType) || 'private',
participantId: this.extractParticipantId(response),
lastMessageId: response.last_message?.id,
lastMessage: response.last_message
? MessageMapper.fromApiResponse(response.last_message)
: undefined,
lastSeq: response.last_seq || 0,
myLastReadSeq: response.my_last_read_seq || 0,
otherLastReadSeq: response.other_last_read_seq || 0,
unreadCount: response.unread_count || 0,
isPinned: response.is_pinned || false,
participants: response.participants?.map(p => this.mapUserFromApi(p)),
group: response.group ? this.mapGroupFromApi(response.group) : undefined,
createdAt: new Date(response.created_at || Date.now()),
updatedAt: new Date(response.updated_at || Date.now()),
};
}
/**
* 将 API 详情响应转换为应用模型
*/
static fromDetailApiResponse(response: ConversationDetailResponse): ConversationModel {
return {
id: String(response.id || ''),
type: (response.type as ConversationType) || 'private',
participantId: this.extractParticipantIdFromDetail(response),
lastMessageId: response.last_message?.id,
lastMessage: response.last_message
? MessageMapper.fromApiResponse(response.last_message)
: undefined,
lastSeq: response.last_seq || 0,
myLastReadSeq: response.my_last_read_seq || 0,
otherLastReadSeq: response.other_last_read_seq || 0,
unreadCount: response.unread_count || 0,
isPinned: response.is_pinned || false,
participants: response.participants?.map(p => this.mapUserFromApi(p)),
group: response.group ? this.mapGroupFromApi(response.group) : undefined,
createdAt: new Date(response.created_at || Date.now()),
updatedAt: new Date(response.updated_at || Date.now()),
};
}
/**
* 将 API 响应数组转换为应用模型数组
*/
static fromApiResponseList(responses: ConversationResponse[]): ConversationModel[] {
return responses.map(r => this.fromApiResponse(r));
}
/**
* 从缓存数据转换为应用模型
*/
static fromCacheData(id: string, cached: ConversationCacheData): ConversationModel {
return {
id: String(cached?.id || id),
type: (cached?.type as ConversationType) || 'private',
participantId: '',
lastSeq: Number(cached?.last_seq || 0),
lastMessage: cached?.last_message
? MessageMapper.fromApiResponse(cached.last_message)
: undefined,
lastMessageId: cached?.last_message?.id,
myLastReadSeq: Number(cached?.my_last_read_seq || 0),
otherLastReadSeq: Number(cached?.other_last_read_seq || 0),
unreadCount: Number(cached?.unread_count || 0),
isPinned: Boolean(cached?.is_pinned),
participants: Array.isArray(cached?.participants)
? cached.participants.map(p => this.mapUserFromApi(p))
: [],
group: cached?.group ? this.mapGroupFromApi(cached.group) : undefined,
createdAt: new Date(cached?.created_at || Date.now()),
updatedAt: new Date(cached?.updated_at || Date.now()),
};
}
/**
* 将数据库记录转换为应用模型
*/
static fromDbRecord(record: ConversationDbRecord): ConversationModel {
return {
id: record.id,
type: 'private', // 数据库中不存储类型,需要额外查询
participantId: record.participantId,
lastMessageId: record.lastMessageId || undefined,
lastSeq: record.lastSeq,
myLastReadSeq: 0,
otherLastReadSeq: 0,
unreadCount: record.unreadCount,
isPinned: false,
createdAt: new Date(record.createdAt),
updatedAt: new Date(record.updatedAt),
};
}
/**
* 将应用模型转换为数据库记录
*/
static toDbRecord(model: ConversationModel): ConversationDbRecord {
return {
id: model.id,
participantId: model.participantId,
lastMessageId: model.lastMessageId || null,
lastSeq: model.lastSeq,
unreadCount: model.unreadCount,
createdAt: model.createdAt.toISOString(),
updatedAt: model.updatedAt.toISOString(),
};
}
/**
* 将应用模型转换为缓存数据
*/
static toCacheData(model: ConversationModel): ConversationCacheData {
return {
id: model.id,
type: model.type,
is_pinned: model.isPinned,
last_seq: model.lastSeq,
last_message: model.lastMessage ? this.messageToCache(model.lastMessage) : undefined,
last_message_at: model.updatedAt.toISOString(),
unread_count: model.unreadCount,
participants: model.participants?.map(p => this.userToCache(p)),
my_last_read_seq: model.myLastReadSeq,
other_last_read_seq: model.otherLastReadSeq,
group: model.group ? this.groupToCache(model.group) : undefined,
created_at: model.createdAt.toISOString(),
updated_at: model.updatedAt.toISOString(),
};
}
/**
* 提取会话参与者 ID
*/
private static extractParticipantId(response: ConversationResponse): string {
if (response.participants && response.participants.length > 0) {
return String(response.participants[0].id || '');
}
return '';
}
/**
* 从详情响应提取参与者 ID
*/
private static extractParticipantIdFromDetail(response: ConversationDetailResponse): string {
if (response.participants && response.participants.length > 0) {
return String(response.participants[0].id || '');
}
return '';
}
/**
* 映射用户 API 响应
*/
private static mapUserFromApi(user: UserDTO): UserModel {
return {
id: String(user.id || ''),
username: user.username || '',
nickname: user.nickname,
avatar: user.avatar ?? undefined,
};
}
/**
* 映射群组 API 响应
*/
private static mapGroupFromApi(group: GroupResponse): GroupModel {
// 将数字类型的 join_type 转换为字符串类型
// JoinType: 0 = 允许任何人, 1 = 需要审批, 2 = 不允许
const mapJoinType = (joinType: number): 'anyone' | 'approval' | 'invite' => {
switch (joinType) {
case 0: return 'anyone';
case 1: return 'approval';
case 2: return 'invite';
default: return 'approval';
}
};
return {
id: String(group.id || ''),
name: group.name || '',
avatar: group.avatar,
description: group.description,
// GroupResponse 没有 announcement 和 updated_at 字段,使用默认值
announcement: undefined,
ownerId: String(group.owner_id || ''),
memberCount: group.member_count || 0,
maxMemberCount: group.max_members || 500,
joinType: mapJoinType(group.join_type),
isMuted: group.mute_all || false,
createdAt: new Date(group.created_at || Date.now()),
updatedAt: new Date(group.created_at || Date.now()),
};
}
/**
* 消息模型转缓存格式
*/
private static messageToCache(message: MessageModel): any {
return {
id: message.id,
conversation_id: message.conversationId,
sender_id: message.senderId,
content: message.content,
message_type: message.type,
is_read: message.isRead,
created_at: message.createdAt.toISOString(),
seq: message.seq,
status: message.status,
segments: message.segments,
reply_to_id: message.replyToId,
sender: message.sender ? this.userToCache(message.sender) : undefined,
};
}
/**
* 用户模型转缓存格式
*/
private static userToCache(user: UserModel): any {
return {
id: user.id,
username: user.username,
nickname: user.nickname,
avatar: user.avatar,
};
}
/**
* 群组模型转缓存格式
*/
private static groupToCache(group: GroupModel): any {
return {
id: group.id,
name: group.name,
avatar: group.avatar,
description: group.description,
announcement: group.announcement,
owner_id: group.ownerId,
member_count: group.memberCount,
max_member_count: group.maxMemberCount,
join_type: group.joinType,
mute_all: group.isMuted,
created_at: group.createdAt.toISOString(),
updated_at: group.updatedAt.toISOString(),
};
}
}

View File

@@ -0,0 +1,194 @@
/**
* 消息数据映射器
* 负责 Message 模型与 API 响应、数据库记录之间的转换
*/
import {
MessageModel,
MessageSegment,
UserModel,
} from '../models';
import type {
MessageResponse,
UserDTO,
} from '../../types/dto';
import { safeParseJsonOrUndefined } from '../../database/core/jsonUtils';
import type { CachedMessage } from '../../database/types/CachedMessage';
// 数据库消息记录类型
export interface MessageDbRecord {
id: string;
conversationId: string;
senderId: string;
content: string | null;
type: string;
isRead: number;
createdAt: string;
seq: number;
status: string;
segments: string | null;
}
export class MessageMapper {
/**
* 将 API 响应转换为应用模型
*/
static fromApiResponse(response: MessageResponse): MessageModel {
const textSegment = response.segments?.find(s => s.type === 'text');
const content = textSegment?.data?.text || textSegment?.data?.content || '';
return {
id: String(response.id || ''),
conversationId: String(response.conversation_id || ''),
senderId: String(response.sender_id || ''),
content,
type: 'text',
isRead: false,
createdAt: new Date(response.created_at || Date.now()),
seq: response.seq || 0,
status: response.status || 'normal',
segments: response.segments || [],
replyToId: response.reply_to_id,
sender: response.sender ? this.mapSenderFromApi(response.sender) : undefined,
};
}
/**
* 将 API 响应数组转换为应用模型数组
*/
static fromApiResponseList(responses: MessageResponse[]): MessageModel[] {
return responses.map(r => this.fromApiResponse(r));
}
/**
* 将数据库记录转换为应用模型
*/
static fromDbRecord(record: MessageDbRecord): MessageModel {
return {
id: record.id,
conversationId: record.conversationId,
senderId: record.senderId,
content: record.content || undefined,
type: record.type || 'text',
isRead: record.isRead === 1,
createdAt: new Date(record.createdAt),
seq: record.seq || 0,
status: record.status || 'normal',
segments: record.segments ? safeParseJsonOrUndefined<MessageSegment[]>(record.segments) : undefined,
};
}
/**
* 将数据库记录数组转换为应用模型数组
*/
static fromDbRecordList(records: MessageDbRecord[]): MessageModel[] {
return records.map(r => this.fromDbRecord(r));
}
/**
* 将应用模型转换为数据库记录
*/
static toDbRecord(model: MessageModel): MessageDbRecord {
return {
id: model.id,
conversationId: model.conversationId,
senderId: model.senderId,
content: model.content || null,
type: model.type,
isRead: model.isRead ? 1 : 0,
createdAt: model.createdAt.toISOString(),
seq: model.seq,
status: model.status,
segments: model.segments ? JSON.stringify(model.segments) : null,
};
}
/**
* 将应用模型转换为 API 请求数据
*/
static toApiRequest(model: Partial<MessageModel>): Record<string, any> {
const request: Record<string, any> = {};
if (model.segments) {
request.segments = model.segments;
}
if (model.content) {
request.content = model.content;
}
if (model.type) {
request.message_type = model.type;
}
if (model.replyToId) {
request.reply_to_id = model.replyToId;
}
return request;
}
/**
* 创建发送消息请求体
*/
static createSendRequest(
detailType: 'private' | 'group',
segments: MessageSegment[],
replyToId?: string
): Record<string, any> {
const body: Record<string, any> = {
detail_type: detailType,
segments,
};
if (replyToId) {
body.reply_to_id = replyToId;
}
return body;
}
/**
* 创建文本消息段
*/
static createTextSegment(text: string): MessageSegment {
return {
type: 'text',
data: { text },
};
}
/**
* 创建图片消息段
*/
static createImageSegment(url: string): MessageSegment {
return {
type: 'image',
data: { url },
};
}
private static mapSenderFromApi(sender: UserDTO): UserModel {
return {
id: String(sender.id || ''),
username: sender.username || '',
nickname: sender.nickname,
avatar: sender.avatar || undefined,
};
}
static toCachedMessage(apiMessage: any, conversationId?: string): CachedMessage {
return {
id: apiMessage.id,
conversationId: apiMessage.conversation_id || conversationId || '',
senderId: apiMessage.sender_id,
content: apiMessage.content,
type: apiMessage.type || 'text',
isRead: apiMessage.is_read || false,
createdAt: apiMessage.created_at,
seq: apiMessage.seq,
status: apiMessage.status || 'normal',
segments: apiMessage.segments,
};
}
static toCachedMessages(apiMessages: any[], conversationId?: string): CachedMessage[] {
return apiMessages.map(m => this.toCachedMessage(m, conversationId));
}
}

View File

@@ -0,0 +1,129 @@
/**
* 帖子数据映射器
* 负责 Post 模型与 API 响应之间的转换
*/
import { PostModel, UserModel } from '../models';
import type { PostDTO } from '../../types/dto';
export class PostMapper {
static fromApiResponse(response: PostDTO): PostModel {
return {
id: String(response.id || ''),
authorId: String(response.user_id || ''),
author: response.author ? this.mapAuthorFromApi(response.author) : undefined,
title: response.title || '',
content: response.content || '',
images: response.images?.map(img => img.url) || [],
likeCount: response.likes_count || 0,
commentCount: response.comments_count || 0,
shareCount: response.shares_count || 0,
viewCount: response.views_count || 0,
favoriteCount: response.favorites_count || 0,
isLiked: response.is_liked || false,
isFavorited: response.is_favorited || false,
isTop: response.is_pinned || false,
status: (response.status || 'published') as 'published' | 'draft' | 'deleted',
channelId: response.channel_id,
channel:
response.channel && response.channel.id
? { id: String(response.channel.id), name: String(response.channel.name || '') }
: undefined,
tags: [],
createdAt: new Date(response.created_at || Date.now()),
// 空字符串/缺省时用 created_at避免误用 Date.now() 导致全部显示「刚修改」
updatedAt: new Date(
response.updated_at ||
response.created_at ||
Date.now()
),
};
}
/**
* 将 API 响应数组转换为应用模型数组
*/
static fromApiResponseList(responses: PostDTO[]): PostModel[] {
return responses.map(r => this.fromApiResponse(r));
}
/**
* 将应用模型转换为 API 请求数据
*/
static toApiRequest(model: Partial<PostModel>): Record<string, any> {
const request: Record<string, any> = {};
if (model.title !== undefined) {
request.title = model.title;
}
if (model.content !== undefined) {
request.content = model.content;
}
if (model.images !== undefined) {
request.images = model.images;
}
if (model.channelId !== undefined) {
request.channel_id = model.channelId;
}
if (model.tags !== undefined) {
request.tags = model.tags;
}
return request;
}
/**
* 创建帖子请求
*/
static createPostRequest(
title: string,
content: string,
images?: string[],
channelId?: string
): Record<string, any> {
const request: Record<string, any> = {
title,
content,
};
if (images && images.length > 0) {
request.images = images;
}
if (channelId) {
request.channel_id = channelId;
}
return request;
}
/**
* 更新帖子请求
*/
static updatePostRequest(
title?: string,
content?: string,
images?: string[]
): Record<string, any> {
const request: Record<string, any> = {};
if (title !== undefined) {
request.title = title;
}
if (content !== undefined) {
request.content = content;
}
if (images !== undefined) {
request.images = images;
}
return request;
}
/**
* 映射作者信息
*/
private static mapAuthorFromApi(author: any): UserModel {
return {
id: String(author.id || ''),
username: author.username || '',
nickname: author.nickname,
avatar: author.avatar,
};
}
}

View File

@@ -0,0 +1,134 @@
/**
* 用户数据映射器
* 负责 User 模型与 API 响应、数据库记录之间的转换
*/
import { UserModel } from '../models';
import type { UserDTO } from '../../types/dto';
// 数据库用户记录类型
export interface UserDbRecord {
id: string;
data: string;
updatedAt: string;
}
export class UserMapper {
static fromDTO(dto: UserDTO): UserModel {
return {
id: String(dto.id || ''),
username: dto.username || '',
nickname: dto.nickname,
avatar: dto.avatar,
bio: dto.bio,
website: dto.website,
location: dto.location,
email: dto.email,
phone: dto.phone,
followersCount: dto.followers_count,
followingCount: dto.following_count,
postsCount: dto.posts_count,
isFollowing: dto.is_following,
createdAt: dto.created_at ? new Date(dto.created_at) : undefined,
};
}
static fromDTOList(dtos: UserDTO[]): UserModel[] {
return dtos.map(dto => this.fromDTO(dto));
}
/**
* 从数据库记录转换为应用模型
*/
static fromDbRecord(record: UserDbRecord): UserModel | null {
try {
const data = JSON.parse(record.data) as UserDTO;
return this.fromDTO(data);
} catch {
return null;
}
}
/**
* 将应用模型转换为数据库记录
*/
static toDbRecord(model: UserModel): UserDbRecord {
const dto: UserDTO = {
id: model.id,
username: model.username,
nickname: model.nickname || '',
avatar: model.avatar || '',
cover_url: '',
bio: model.bio || '',
website: model.website || '',
location: model.location || '',
email: model.email,
phone: model.phone,
followers_count: model.followersCount || 0,
following_count: model.followingCount || 0,
posts_count: model.postsCount || 0,
is_following: model.isFollowing,
created_at: model.createdAt?.toISOString() || '',
};
return {
id: model.id,
data: JSON.stringify(dto),
updatedAt: new Date().toISOString(),
};
}
/**
* 将应用模型转换为 API 请求数据
*/
static toApiRequest(model: Partial<UserModel>): Record<string, any> {
const request: Record<string, any> = {};
if (model.nickname !== undefined) {
request.nickname = model.nickname;
}
if (model.avatar !== undefined) {
request.avatar = model.avatar;
}
if (model.bio !== undefined) {
request.bio = model.bio;
}
if (model.website !== undefined) {
request.website = model.website;
}
if (model.location !== undefined) {
request.location = model.location;
}
if (model.phone !== undefined) {
request.phone = model.phone;
}
if (model.email !== undefined) {
request.email = model.email;
}
return request;
}
/**
* 将应用模型转换为 DTO
*/
static toDTO(model: UserModel): UserDTO {
return {
id: model.id,
username: model.username,
nickname: model.nickname || '',
avatar: model.avatar || '',
cover_url: '',
bio: model.bio || '',
website: model.website || '',
location: model.location || '',
email: model.email,
phone: model.phone,
followers_count: model.followersCount || 0,
following_count: model.followingCount || 0,
posts_count: model.postsCount || 0,
is_following: model.isFollowing,
created_at: model.createdAt?.toISOString() || '',
};
}
}

View File

@@ -0,0 +1,9 @@
/**
* 映射器导出
* 统一导出所有数据映射器
*/
export * from './MessageMapper';
export * from './ConversationMapper';
export * from './UserMapper';
export * from './PostMapper';

View File

@@ -0,0 +1,93 @@
/**
* 消息 Repository 接口
* 定义消息数据访问的抽象
*/
import type { Message, Conversation } from '../../../core/entities/Message';
export interface IMessageRepository {
// ==================== 消息操作 ====================
/**
* 获取会话的消息列表
*/
getMessages(
conversationId: string,
options?: {
beforeSeq?: number;
afterSeq?: number;
limit?: number;
}
): Promise<Message[]>;
/**
* 保存消息
*/
saveMessage(message: Message): Promise<void>;
/**
* 批量保存消息
*/
saveMessages(messages: Message[]): Promise<void>;
/**
* 更新消息状态
*/
updateMessageStatus(
messageId: string,
status: 'normal' | 'recalled' | 'deleted'
): Promise<void>;
/**
* 标记消息为已读
*/
markAsRead(messageIds: string[]): Promise<void>;
/**
* 获取未读消息数
*/
getUnreadCount(conversationId: string): Promise<number>;
// ==================== 会话操作 ====================
/**
* 获取会话列表
*/
getConversations(): Promise<Conversation[]>;
/**
* 获取单个会话
*/
getConversation(conversationId: string): Promise<Conversation | null>;
/**
* 保存会话
*/
saveConversation(conversation: Conversation): Promise<void>;
/**
* 更新会话未读数
*/
updateUnreadCount(conversationId: string, count: number): Promise<void>;
/**
* 更新会话最后消息
*/
updateLastMessage(
conversationId: string,
messageId: string,
seq: number
): Promise<void>;
// ==================== 同步操作 ====================
/**
* 从服务器同步消息
*/
syncMessages(conversationId: string, lastSeq: number): Promise<Message[]>;
/**
* 获取服务器最新消息序列号
*/
getServerLastSeq(conversationId: string): Promise<number>;
}

View File

@@ -0,0 +1,190 @@
/**
* 帖子 Repository 接口
* 定义帖子数据访问的抽象
*/
import type { Post, PostImage, PostStatus } from '../../../core/entities/Post';
// ==================== 请求/响应类型 ====================
/**
* 获取帖子列表参数
*/
export interface GetPostsParams {
/** 分页 - 页码从1开始 */
page?: number;
/** 分页 - 每页数量 */
pageSize?: number;
/** 游标 - 用于无限滚动加载 */
cursor?: string;
/** 帖子类型筛选可选follow, hot, latest */
post_type?: 'follow' | 'hot' | 'latest';
/** 频道ID过滤 */
channelId?: string;
/** 作者ID过滤 */
authorId?: string;
/** 标签过滤 */
tags?: string[];
/** 状态过滤 */
status?: PostStatus;
/** 排序字段 */
sortBy?: 'createdAt' | 'likesCount' | 'commentsCount' | 'viewsCount';
/** 排序方向 */
sortOrder?: 'asc' | 'desc';
/** 是否只获取置顶帖子 */
pinnedOnly?: boolean;
}
/**
* 创建帖子数据
*/
export interface CreatePostData {
/** 帖子标题 */
title: string;
/** 帖子内容 */
content: string;
/** 图片列表 */
images?: PostImage[];
/** 所属频道ID */
channelId?: string;
/** 标签列表 */
tags?: string[];
/** 帖子状态 */
status?: PostStatus;
}
/**
* 更新帖子数据
*/
export interface UpdatePostData {
/** 帖子标题 */
title?: string;
/** 帖子内容 */
content?: string;
/** 图片列表 */
images?: PostImage[];
/** 标签列表 */
tags?: string[];
/** 帖子状态 */
status?: PostStatus;
/** 是否置顶 */
isPinned?: boolean;
}
/**
* 帖子列表结果
*/
export interface PostsResult {
/** 帖子列表 */
posts: Post[];
/** 是否有更多数据 */
hasMore: boolean;
/** 下一页游标 */
nextCursor?: string;
/** 总数(如果支持) */
total?: number;
}
/**
* 搜索帖子参数
*/
export interface SearchPostsParams {
/** 搜索关键词 */
keyword: string;
/** 分页 - 页码 */
page?: number;
/** 分页 - 每页数量 */
pageSize?: number;
/** 搜索范围:标题、内容、或全部 */
scope?: 'title' | 'content' | 'all';
/** 频道ID过滤 */
channelId?: string;
}
// ==================== Repository 接口 ====================
export interface IPostRepository {
// ==================== 帖子查询 ====================
/**
* 获取帖子列表
* @param params 查询参数
* @returns 帖子列表结果
*/
getPosts(params?: GetPostsParams): Promise<PostsResult>;
/**
* 获取单个帖子详情
* @param id 帖子ID
* @returns 帖子实体不存在则返回null
*/
getPostById(id: string): Promise<Post | null>;
/**
* 获取用户发布的帖子列表
* @param userId 用户ID
* @param params 查询参数
* @returns 帖子列表结果
*/
getPostsByUser(userId: string, params?: GetPostsParams): Promise<PostsResult>;
/**
* 搜索帖子
* @param params 搜索参数
* @returns 帖子列表结果
*/
searchPosts(params: SearchPostsParams): Promise<PostsResult>;
// ==================== 帖子操作 ====================
/**
* 创建帖子
* @param data 创建帖子数据
* @returns 创建的帖子实体
*/
createPost(data: CreatePostData): Promise<Post>;
/**
* 更新帖子
* @param id 帖子ID
* @param data 更新数据
* @returns 更新后的帖子实体
*/
updatePost(id: string, data: UpdatePostData): Promise<Post>;
/**
* 删除帖子
* @param id 帖子ID
*/
deletePost(id: string): Promise<void>;
// ==================== 互动操作 ====================
/**
* 点赞帖子
* @param id 帖子ID
* @returns 更新后的帖子实体
*/
likePost(id: string): Promise<Post>;
/**
* 取消点赞
* @param id 帖子ID
* @returns 更新后的帖子实体
*/
unlikePost(id: string): Promise<Post>;
/**
* 收藏帖子
* @param id 帖子ID
* @returns 更新后的帖子实体
*/
favoritePost(id: string): Promise<Post>;
/**
* 取消收藏
* @param id 帖子ID
* @returns 更新后的帖子实体
*/
unfavoritePost(id: string): Promise<Post>;
}