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

@@ -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 {