refactor: restructure core architecture and responsive system
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:
@@ -3,12 +3,11 @@
|
||||
* 处理用户身份认证申请、状态查询等功能
|
||||
*/
|
||||
|
||||
import { api } from '../core/api';
|
||||
import { api, PaginatedData } from '../core/api';
|
||||
import {
|
||||
VerificationStatusDTO,
|
||||
VerificationRecordDTO,
|
||||
SubmitVerificationRequest,
|
||||
PaginatedData,
|
||||
} from '@/types/dto';
|
||||
|
||||
// 提交认证响应
|
||||
|
||||
@@ -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() {
|
||||
|
||||
98
src/services/core/datasources/ApiDataSource.ts
Normal file
98
src/services/core/datasources/ApiDataSource.ts
Normal 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();
|
||||
199
src/services/core/datasources/CacheDataSource.ts
Normal file
199
src/services/core/datasources/CacheDataSource.ts
Normal 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();
|
||||
184
src/services/core/datasources/WSClient.ts
Normal file
184
src/services/core/datasources/WSClient.ts
Normal 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;
|
||||
9
src/services/core/datasources/index.ts
Normal file
9
src/services/core/datasources/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 数据源导出
|
||||
* 统一导出所有数据源实现
|
||||
*/
|
||||
|
||||
export * from './interfaces';
|
||||
export * from './ApiDataSource';
|
||||
export { LocalDataSource, localDataSource } from '@/database';
|
||||
export * from './CacheDataSource';
|
||||
68
src/services/core/datasources/interfaces.ts
Normal file
68
src/services/core/datasources/interfaces.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
12
src/services/core/verification.ts
Normal file
12
src/services/core/verification.ts
Normal 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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
302
src/services/mappers/ConversationMapper.ts
Normal file
302
src/services/mappers/ConversationMapper.ts
Normal 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
194
src/services/mappers/MessageMapper.ts
Normal file
194
src/services/mappers/MessageMapper.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
129
src/services/mappers/PostMapper.ts
Normal file
129
src/services/mappers/PostMapper.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
134
src/services/mappers/UserMapper.ts
Normal file
134
src/services/mappers/UserMapper.ts
Normal 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() || '',
|
||||
};
|
||||
}
|
||||
}
|
||||
9
src/services/mappers/index.ts
Normal file
9
src/services/mappers/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 映射器导出
|
||||
* 统一导出所有数据映射器
|
||||
*/
|
||||
|
||||
export * from './MessageMapper';
|
||||
export * from './ConversationMapper';
|
||||
export * from './UserMapper';
|
||||
export * from './PostMapper';
|
||||
93
src/services/message/types/repository.ts
Normal file
93
src/services/message/types/repository.ts
Normal 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>;
|
||||
}
|
||||
190
src/services/post/types/repository.ts
Normal file
190
src/services/post/types/repository.ts
Normal 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>;
|
||||
}
|
||||
Reference in New Issue
Block a user