Initial frontend repository commit.

Include app source and update .gitignore to exclude local release artifacts and signing files.

Made-with: Cursor
This commit is contained in:
2026-03-09 21:29:03 +08:00
commit 3968660048
129 changed files with 55599 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
import { Alert } from 'react-native';
import type { AlertButton, AlertOptions } from 'react-native';
import { showDialog } from './dialogService';
import { showPrompt } from './promptService';
let installed = false;
const resolvePromptType = (title?: string) => {
const source = title || '';
if (source.includes('成功') || source.includes('完成') || source.includes('已')) return 'success' as const;
if (source.includes('失败') || source.includes('错误') || source.includes('无法')) return 'error' as const;
if (source.includes('请稍候') || source.includes('提示') || source.includes('权限')) return 'warning' as const;
return 'info' as const;
};
export const installAlertOverride = () => {
if (installed) return;
installed = true;
const nativeAlert = Alert.alert.bind(Alert);
Alert.alert = (
title?: string,
message?: string,
buttons?: AlertButton[],
options?: AlertOptions
) => {
const nextTitle = title || '提示';
const nextMessage = message || '';
const actionButtons = buttons ?? [];
if (actionButtons.length === 0) {
showPrompt({
title: nextTitle,
message: nextMessage || nextTitle,
type: resolvePromptType(nextTitle),
});
return;
}
if (actionButtons.length <= 3) {
showDialog({
title: nextTitle,
message: nextMessage,
actions: actionButtons,
options,
});
return;
}
// 极少数复杂按钮场景,保底回退原生 Alert
nativeAlert(nextTitle, nextMessage, buttons, options);
};
};

316
src/services/api.ts Normal file
View File

@@ -0,0 +1,316 @@
/**
* API 客户端
* 使用 fetch API 进行 HTTP 请求
* 支持请求/响应拦截器
*/
import AsyncStorage from '@react-native-async-storage/async-storage';
import { CommonActions } from '@react-navigation/native';
import Constants from 'expo-constants';
const getBaseUrl = () => {
const configuredBaseUrl = Constants.expoConfig?.extra?.apiBaseUrl;
if (typeof configuredBaseUrl === 'string' && configuredBaseUrl.trim().length > 0) {
return configuredBaseUrl;
}
return 'https://bbs.littlelan.cn/api/v1';
};
const getWsUrl = () => {
const configuredWsUrl = Constants.expoConfig?.extra?.wsUrl;
if (typeof configuredWsUrl === 'string' && configuredWsUrl.trim().length > 0) {
return configuredWsUrl;
}
return 'wss://bbs.littlelan.cn/ws';
};
const BASE_URL = getBaseUrl();
const WS_URL = getWsUrl();
// Token 存储键
const TOKEN_KEY = 'auth_token';
const REFRESH_TOKEN_KEY = 'refresh_token';
// API 响应格式
export interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
// 分页响应格式
export interface PaginatedData<T> {
list: T[];
total: number;
page: number;
page_size: number;
total_pages: number;
}
// 错误类型
export class ApiError extends Error {
code: number;
message: string;
constructor(code: number, message: string) {
super(message);
this.code = code;
this.message = message;
this.name = 'ApiError';
}
}
// API 客户端类
class ApiClient {
private baseUrl: string;
private navigation: any = null;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
// 设置导航对象用于处理401跳转
setNavigation(navigation: any) {
this.navigation = navigation;
}
// 获取 token
async getToken(): Promise<string | null> {
try {
return await AsyncStorage.getItem(TOKEN_KEY);
} catch (error) {
console.error('获取token失败:', error);
return null;
}
}
// 保存 token
async setToken(token: string): Promise<void> {
try {
await AsyncStorage.setItem(TOKEN_KEY, token);
} catch (error) {
console.error('保存token失败:', error);
}
}
// 获取刷新 token
async getRefreshToken(): Promise<string | null> {
try {
return await AsyncStorage.getItem(REFRESH_TOKEN_KEY);
} catch (error) {
console.error('获取刷新token失败:', error);
return null;
}
}
// 保存刷新 token
async setRefreshToken(token: string): Promise<void> {
try {
await AsyncStorage.setItem(REFRESH_TOKEN_KEY, token);
} catch (error) {
console.error('保存刷新token失败:', error);
}
}
// 清除 token
async clearToken(): Promise<void> {
try {
await AsyncStorage.multiRemove([TOKEN_KEY, REFRESH_TOKEN_KEY]);
} catch (error) {
console.error('清除token失败:', error);
}
}
// 统一请求方法
private async request<T>(
method: string,
path: string,
params?: Record<string, any>,
body?: any
): Promise<ApiResponse<T>> {
const url = new URL(`${this.baseUrl}${path}`);
// 添加查询参数
if (params) {
Object.keys(params).forEach(key => {
if (params[key] !== undefined && params[key] !== null) {
url.searchParams.append(key, String(params[key]));
}
});
}
// 构建请求头
const headers: HeadersInit = {
'Content-Type': 'application/json',
};
// 添加认证 token
const token = await this.getToken();
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
// 构建请求配置
const config: RequestInit = {
method,
headers,
};
// 添加请求体
if (body && method !== 'GET') {
config.body = JSON.stringify(body);
}
try {
const response = await fetch(url.toString(), config);
// 处理 401 未授权
if (response.status === 401) {
// 尝试刷新 token
const refreshed = await this.refreshToken();
if (!refreshed) {
// 刷新失败,清除 token 并跳转登录
await this.clearToken();
if (this.navigation) {
this.navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: 'Login' }],
})
);
}
throw new ApiError(401, '登录已过期,请重新登录');
}
// 刷新成功后重试请求
return this.request(method, path, params, body);
}
// 解析响应
const data: ApiResponse<T> = await response.json();
// 处理业务错误
if (data.code !== 0) {
throw new ApiError(data.code, data.message);
}
return data;
} catch (error) {
// 如果是 ApiError直接抛出
if (error instanceof ApiError) {
throw error;
}
// 网络错误
console.error('API请求失败:', error);
throw new ApiError(500, '网络请求失败,请检查网络连接');
}
}
// 刷新 token
private async refreshToken(): Promise<boolean> {
try {
const refreshToken = await this.getRefreshToken();
if (!refreshToken) {
return false;
}
const response = await fetch(`${this.baseUrl}/auth/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${refreshToken}`,
},
});
if (!response.ok) {
return false;
}
const data = await response.json();
if (data.code === 0 && data.data?.token) {
await this.setToken(data.data.token);
// 后端返回refresh_token (snake_case),兼容两种命名
if (data.data.refresh_token || data.data.refreshToken) {
await this.setRefreshToken(data.data.refresh_token || data.data.refreshToken);
}
return true;
}
return false;
} catch (error) {
console.error('刷新token失败:', error);
return false;
}
}
// GET 请求
async get<T>(path: string, params?: Record<string, any>): Promise<ApiResponse<T>> {
return this.request<T>('GET', path, params);
}
// POST 请求
async post<T>(path: string, body?: any): Promise<ApiResponse<T>> {
return this.request<T>('POST', path, undefined, body);
}
// PUT 请求
async put<T>(path: string, body?: any): Promise<ApiResponse<T>> {
return this.request<T>('PUT', path, undefined, body);
}
// DELETE 请求
async delete<T>(path: string, body?: any): Promise<ApiResponse<T>> {
return this.request<T>('DELETE', path, undefined, body);
}
// 上传文件
async upload<T>(
path: string,
file: { uri: string; name: string; type: string },
additionalData?: Record<string, string>
): Promise<ApiResponse<T>> {
const formData = new FormData();
// 添加文件使用image字段名
formData.append('image', {
uri: file.uri,
name: file.name,
type: file.type,
} as any);
// 添加额外数据
if (additionalData) {
Object.keys(additionalData).forEach(key => {
formData.append(key, additionalData[key]);
});
}
const token = await this.getToken();
const headers: HeadersInit = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`${this.baseUrl}${path}`, {
method: 'POST',
headers,
body: formData,
});
const data: ApiResponse<T> = await response.json();
if (data.code !== 0) {
throw new ApiError(data.code, data.message);
}
return data;
}
}
// 导出 API 客户端实例
export const api = new ApiClient(BASE_URL);
// 导出 WebSocket URL
export { WS_URL, TOKEN_KEY, REFRESH_TOKEN_KEY };

436
src/services/authService.ts Normal file
View File

@@ -0,0 +1,436 @@
/**
* 认证服务
* 处理用户登录、注册、登出等认证功能
*/
import { api } from './api';
import { User } from '../types';
export function resolveAuthApiError(error: any, fallback = '操作失败,请稍后重试'): string {
const code: number = error?.code ?? 0;
const msg: string = String(error?.message ?? '').toLowerCase();
if (msg.includes('network request failed') || msg.includes('failed to fetch')) {
return '网络连接失败,请检查网络后重试';
}
if (code === 429 || msg.includes('too frequently')) {
return '验证码发送过于频繁,请稍后再试';
}
if (msg.includes('verification code expired')) {
return '验证码已过期,请重新获取';
}
if (msg.includes('invalid verification code')) {
return '验证码错误,请重新输入';
}
if (msg.includes('email already exists')) {
return '该邮箱已被注册,请直接登录';
}
if (msg.includes('email not bound')) {
return '当前账号未绑定邮箱,请先完成邮箱绑定验证';
}
if (msg.includes('user not found')) {
return '该邮箱尚未注册';
}
if (msg.includes('email service unavailable')) {
return '邮件服务暂不可用,请稍后再试';
}
if (msg.includes('invalid email')) {
return '邮箱格式不正确';
}
if (msg.includes('invalid username or password')) {
return '账号或密码错误';
}
if (typeof error?.message === 'string' && error.message.trim().length > 0) {
return error.message;
}
return fallback;
}
// 登录请求参数
export interface LoginRequest {
username?: string;
account?: string;
password: string;
}
// 注册请求参数
export interface RegisterRequest {
username: string;
password: string;
nickname: string;
email: string;
verification_code: string;
phone?: string;
}
export interface SendEmailCodeRequest {
email: string;
}
export interface ResetPasswordRequest {
email: string;
verification_code: string;
new_password: string;
}
export interface VerifyCurrentUserEmailRequest {
email: string;
verification_code: string;
}
// 认证响应
export interface AuthResponse {
token: string;
refreshToken?: string;
refresh_token?: string; // 兼容后端的snake_case
user: User;
}
// 刷新Token响应
export interface RefreshTokenResponse {
token: string;
refreshToken?: string;
refresh_token?: string; // 兼容后端的snake_case
}
// 用户信息响应 - API直接返回User对象不需要包装
export type UserResponse = User;
// 更新用户信息请求
export interface UpdateUserRequest {
nickname?: string;
avatar?: string;
bio?: string;
website?: string;
location?: string;
phone?: string;
email?: string;
}
export interface BlockedUserListResponse {
list: User[];
total: number;
page: number;
page_size: number;
total_pages: number;
}
export interface BlockStatusResponse {
is_blocked: boolean;
}
// 认证服务类
// 职责:纯 HTTP 层 + Token 管理,不涉及 SQLite
// SQLite 的读写编排由各领域 Manager/store 统一负责
class AuthService {
// ── 内部辅助:从服务器获取用户 ──
private async refreshCurrentUserFromServer(): Promise<User | null> {
try {
const response = await api.get<UserResponse>('/users/me');
return response.data;
} catch (error) {
console.error('[AuthService] 获取当前用户失败:', error);
return null;
}
}
private async refreshUserByIdFromServer(userId: string): Promise<User | null> {
try {
const response = await api.get<UserResponse>(`/users/${userId}`);
return response.data;
} catch (error) {
console.error('[AuthService] 获取用户信息失败:', error);
return null;
}
}
// ── 公开 API仅做 HTTP 请求 + Token 存储,不碰 SQLite ──
// 用户登录(只管 API + TokenDB 操作交给 authStore
async login(data: LoginRequest): Promise<AuthResponse> {
const response = await api.post<AuthResponse>('/auth/login', data);
if (response.data.token) {
await api.setToken(response.data.token);
}
const refreshToken = response.data.refresh_token || response.data.refreshToken;
if (refreshToken) {
await api.setRefreshToken(refreshToken);
}
return response.data;
}
// 用户注册(只管 API + TokenDB 操作交给 authStore
async register(data: RegisterRequest): Promise<AuthResponse> {
const response = await api.post<AuthResponse>('/auth/register', data);
if (response.data.token) {
await api.setToken(response.data.token);
}
const refreshToken = response.data.refresh_token || response.data.refreshToken;
if (refreshToken) {
await api.setRefreshToken(refreshToken);
}
return response.data;
}
// 发送注册验证码
async sendRegisterCode(email: string): Promise<boolean> {
const response = await api.post<{ success: boolean }>('/auth/register/send-code', { email } as SendEmailCodeRequest);
return response.code === 0;
}
// 发送找回密码验证码
async sendPasswordResetCode(email: string): Promise<boolean> {
const response = await api.post<{ success: boolean }>('/auth/password/send-code', { email } as SendEmailCodeRequest);
return response.code === 0;
}
// 找回密码重置
async resetPassword(data: ResetPasswordRequest): Promise<boolean> {
const response = await api.post<{ success: boolean }>('/auth/password/reset', data);
return response.code === 0;
}
// 已登录用户发送邮箱验证验证码
async sendCurrentUserEmailVerifyCode(email: string): Promise<boolean> {
const response = await api.post<{ success: boolean }>('/users/me/email/send-code', { email } as SendEmailCodeRequest);
return response.code === 0;
}
// 已登录用户提交邮箱验证码
async verifyCurrentUserEmail(data: VerifyCurrentUserEmailRequest): Promise<boolean> {
const response = await api.post<{ success: boolean }>('/users/me/email/verify', data);
return response.code === 0;
}
// 用户登出(只管 API + TokenDB/缓存清理交给 authStore
async logout(): Promise<void> {
try {
await api.post('/auth/logout');
} catch (error) {
console.error('[AuthService] 登出 API 失败:', error);
} finally {
await api.clearToken();
}
}
// 纯 API 获取当前用户(无任何 DB 依赖,供冷启动校验 Token 时使用)
async fetchCurrentUserFromAPI(): Promise<User | null> {
try {
const response = await api.get<UserResponse>('/users/me');
return response.data;
} catch (error) {
console.error('[AuthService] fetchCurrentUserFromAPI 失败:', error);
return null;
}
}
// 刷新Token
async refreshToken(): Promise<RefreshTokenResponse | null> {
try {
const response = await api.post<RefreshTokenResponse>('/auth/refresh');
if (response.data.token) {
await api.setToken(response.data.token);
}
if (response.data.refreshToken) {
await api.setRefreshToken(response.data.refreshToken);
}
return response.data;
} catch (error) {
console.error('刷新Token失败:', error);
await api.clearToken();
return null;
}
}
// 获取当前用户信息(兼容旧调用,始终走服务端)
async getCurrentUser(): Promise<User | null> {
return this.refreshCurrentUserFromServer();
}
// 强制从服务器获取当前用户信息(别名,保留兼容)
async fetchCurrentUserFresh(): Promise<User | null> {
return this.fetchCurrentUserFromAPI();
}
async fetchUserByIdFromAPI(userId: string): Promise<User | null> {
return this.refreshUserByIdFromServer(userId);
}
// 获取用户信息根据用户ID
async getUserById(userId: string, forceRefresh = false): Promise<User | null> {
void forceRefresh; // 兼容旧签名:缓存由 userManager 统一编排
return this.refreshUserByIdFromServer(userId);
}
// 更新用户信息
async updateUser(data: UpdateUserRequest): Promise<User | null> {
try {
const response = await api.put<UserResponse>('/users/me', data);
return response.data;
} catch (error) {
console.error('更新用户信息失败:', error);
return null;
}
}
// 修改密码
async sendChangePasswordCode(): Promise<boolean> {
const response = await api.post<{ success: boolean }>('/users/change-password/send-code');
return response.code === 0;
}
// 修改密码(需要邮箱验证码)
async changePassword(oldPassword: string, newPassword: string, verificationCode: string): Promise<boolean> {
try {
const response = await api.post('/users/change-password', {
old_password: oldPassword,
new_password: newPassword,
verification_code: verificationCode,
});
return response.code === 0;
} catch (error) {
console.error('修改密码失败:', error);
return false;
}
}
// 检查用户名是否可用
async checkUsernameAvailable(username: string): Promise<boolean> {
try {
const response = await api.get<{ available: boolean }>('/auth/check-username', {
username,
});
return response.data.available;
} catch (error) {
console.error('检查用户名失败:', error);
return false;
}
}
// 关注用户
async followUser(userId: string): Promise<boolean> {
try {
const response = await api.post(`/users/${userId}/follow`);
return response.code === 0;
} catch (error) {
console.error('关注用户失败:', error);
return false;
}
}
// 取消关注用户
async unfollowUser(userId: string): Promise<boolean> {
try {
const response = await api.delete(`/users/${userId}/follow`);
return response.code === 0;
} catch (error) {
console.error('取消关注用户失败:', error);
return false;
}
}
// 获取用户关注列表
async getFollowingList(userId: string, page = 1, pageSize = 20): Promise<User[]> {
try {
const response = await api.get<{ list: User[] }>(`/users/${userId}/following`, {
page,
page_size: pageSize,
});
return response.data.list;
} catch (error) {
console.error('获取关注列表失败:', error);
return [];
}
}
// 获取用户粉丝列表
async getFollowersList(userId: string, page = 1, pageSize = 20): Promise<User[]> {
try {
const response = await api.get<{ list: User[] }>(`/users/${userId}/followers`, {
page,
page_size: pageSize,
});
return response.data.list;
} catch (error) {
console.error('获取粉丝列表失败:', error);
return [];
}
}
// 拉黑用户
async blockUser(userId: string): Promise<boolean> {
try {
const response = await api.post(`/users/${userId}/block`);
return response.code === 0;
} catch (error) {
console.error('拉黑用户失败:', error);
return false;
}
}
// 取消拉黑
async unblockUser(userId: string): Promise<boolean> {
try {
const response = await api.delete(`/users/${userId}/block`);
return response.code === 0;
} catch (error) {
console.error('取消拉黑失败:', error);
return false;
}
}
// 获取黑名单
async getBlockedUsers(page = 1, pageSize = 20): Promise<BlockedUserListResponse> {
try {
const response = await api.get<BlockedUserListResponse>('/users/blocks', {
page,
page_size: pageSize,
});
return response.data;
} catch (error) {
console.error('获取黑名单失败:', error);
return {
list: [],
total: 0,
page,
page_size: pageSize,
total_pages: 0,
};
}
}
// 获取拉黑状态
async getBlockStatus(userId: string): Promise<boolean> {
try {
const response = await api.get<BlockStatusResponse>(`/users/${userId}/block-status`);
return !!response.data.is_blocked;
} catch (error) {
console.error('获取拉黑状态失败:', error);
return false;
}
}
// 搜索用户
async searchUsers(keyword: string, page = 1, pageSize = 20): Promise<{ list: User[]; total: number }> {
try {
const response = await api.get<{ list: User[]; total: number }>('/users/search', {
keyword,
page,
page_size: pageSize,
});
return response.data;
} catch (error) {
console.error('搜索用户失败:', error);
return { list: [], total: 0 };
}
}
}
// 导出认证服务实例
export const authService = new AuthService();

View File

@@ -0,0 +1,319 @@
/**
* 后台保活服务
* 使用 expo-background-fetch 和 expo-task-manager 实现 App 后台保活
*
* 功能:
* - 定期后台任务保持 App 在内存中
* - 配合 WebSocket 服务保持长连接
* - 收到消息时触发震动提示
*/
import { AppState, AppStateStatus, Platform } from 'react-native';
import * as BackgroundFetch from 'expo-background-fetch';
import * as TaskManager from 'expo-task-manager';
import * as Haptics from 'expo-haptics';
import { websocketService } from './websocketService';
// 后台任务名称
const BACKGROUND_FETCH_TASK = 'background-fetch-keepalive';
const WEBSOCKET_KEEPALIVE_TASK = 'websocket-keepalive';
// 后台任务间隔Android 最小 15 分钟iOS 最小 15 分钟)
const BACKGROUND_INTERVAL = 15; // 15 分钟
// 震动配置
interface VibrationConfig {
enabled: boolean; // 是否启用震动
onChatMessage: boolean; // 私聊消息震动
onGroupMessage: boolean; // 群聊消息震动
onNotification: boolean; // 系统通知震动
style: Haptics.ImpactFeedbackStyle; // 震动样式
}
// 默认震动配置
const defaultVibrationConfig: VibrationConfig = {
enabled: true,
onChatMessage: true,
onGroupMessage: true,
onNotification: true,
style: Haptics.ImpactFeedbackStyle.Light,
};
// 后台服务状态
let isInitialized = false;
let vibrationConfig: VibrationConfig = { ...defaultVibrationConfig };
let appStateSubscription: any = null;
// 定义后台任务
TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
console.log('[BackgroundService] 后台任务执行中...');
try {
// 检查 WebSocket 连接状态
if (!websocketService.isConnected()) {
console.log('[BackgroundService] WebSocket 断开,尝试重连');
await websocketService.connect();
}
// 返回收到新数据
return BackgroundFetch.BackgroundFetchResult.NewData;
} catch (error) {
console.error('[BackgroundService] 后台任务执行失败:', error);
return BackgroundFetch.BackgroundFetchResult.Failed;
}
});
// WebSocket 保活任务
TaskManager.defineTask(WEBSOCKET_KEEPALIVE_TASK, async () => {
console.log('[BackgroundService] WebSocket 保活任务执行...');
try {
if (!websocketService.isConnected()) {
console.log('[BackgroundService] WebSocket 断开,尝试重连');
await websocketService.connect();
}
return BackgroundFetch.BackgroundFetchResult.NewData;
} catch (error) {
console.error('[BackgroundService] WebSocket 保活失败:', error);
return BackgroundFetch.BackgroundFetchResult.Failed;
}
});
/**
* 触发震动反馈
* @param type 震动类型
*/
export async function triggerVibration(
type: 'light' | 'medium' | 'heavy' | 'success' | 'warning' | 'error' = 'light'
): Promise<void> {
if (!vibrationConfig.enabled) {
return;
}
try {
switch (type) {
case 'light':
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
break;
case 'medium':
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
break;
case 'heavy':
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
break;
case 'success':
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
break;
case 'warning':
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
break;
case 'error':
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
break;
}
} catch (error) {
console.error('[BackgroundService] 震动反馈失败:', error);
}
}
/**
* 收到消息时触发震动
* @param messageType 消息类型
*/
export async function vibrateOnMessage(messageType: 'chat' | 'group_message' | 'notification'): Promise<void> {
if (!vibrationConfig.enabled) {
return;
}
switch (messageType) {
case 'chat':
if (vibrationConfig.onChatMessage) {
await triggerVibration('light');
}
break;
case 'group_message':
if (vibrationConfig.onGroupMessage) {
await triggerVibration('light');
}
break;
case 'notification':
if (vibrationConfig.onNotification) {
await triggerVibration('medium');
}
break;
}
}
/**
* 配置震动设置
*/
export function setVibrationConfig(config: Partial<VibrationConfig>): void {
vibrationConfig = { ...vibrationConfig, ...config };
}
/**
* 获取当前震动配置
*/
export function getVibrationConfig(): VibrationConfig {
return { ...vibrationConfig };
}
/**
* 启用/禁用震动
*/
export function setVibrationEnabled(enabled: boolean): void {
vibrationConfig.enabled = enabled;
}
/**
* 注册后台任务
*/
async function registerBackgroundTasks(): Promise<void> {
try {
// 检查是否已注册
const isRegistered = await TaskManager.isTaskRegisteredAsync(BACKGROUND_FETCH_TASK);
if (!isRegistered) {
await BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK, {
minimumInterval: BACKGROUND_INTERVAL * 60, // 转换为秒
stopOnTerminate: false, // App 终止后继续运行
startOnBoot: true, // 设备启动后自动运行
});
console.log('[BackgroundService] 后台任务注册成功');
}
// 注册 WebSocket 保活任务
const isWsKeepaliveRegistered = await TaskManager.isTaskRegisteredAsync(WEBSOCKET_KEEPALIVE_TASK);
if (!isWsKeepaliveRegistered) {
await BackgroundFetch.registerTaskAsync(WEBSOCKET_KEEPALIVE_TASK, {
minimumInterval: 60, // 1 分钟检查一次
stopOnTerminate: false,
startOnBoot: true,
});
console.log('[BackgroundService] WebSocket 保活任务注册成功');
}
} catch (error) {
console.error('[BackgroundService] 注册后台任务失败:', error);
}
}
/**
* 取消后台任务
*/
async function unregisterBackgroundTasks(): Promise<void> {
try {
await BackgroundFetch.unregisterTaskAsync(BACKGROUND_FETCH_TASK);
await BackgroundFetch.unregisterTaskAsync(WEBSOCKET_KEEPALIVE_TASK);
console.log('[BackgroundService] 后台任务已取消');
} catch (error) {
console.error('[BackgroundService] 取消后台任务失败:', error);
}
}
/**
* 设置 App 状态监听
*/
function setupAppStateListener(): void {
if (appStateSubscription) {
return;
}
appStateSubscription = AppState.addEventListener('change', (nextAppState: AppStateStatus) => {
console.log('[BackgroundService] App 状态变化:', nextAppState);
if (nextAppState === 'active') {
// App 回到前台,确保连接
console.log('[BackgroundService] App 回到前台');
if (!websocketService.isConnected()) {
websocketService.connect();
}
} else if (nextAppState === 'background') {
// App 进入后台,启动保活
console.log('[BackgroundService] App 进入后台,启动保活机制');
}
});
}
/**
* 初始化后台保活服务
*/
export async function initBackgroundService(): Promise<boolean> {
if (isInitialized) {
console.log('[BackgroundService] 服务已初始化');
return true;
}
try {
console.log('[BackgroundService] 初始化后台保活服务...');
// 检查后台任务状态
const status = await BackgroundFetch.getStatusAsync();
if (status !== BackgroundFetch.BackgroundFetchStatus.Available) {
console.warn('[BackgroundService] 后台任务不可用,状态:', status);
// 即使后台任务不可用,也继续初始化其他功能
}
// 注册后台任务
await registerBackgroundTasks();
// 设置 App 状态监听
setupAppStateListener();
isInitialized = true;
console.log('[BackgroundService] 后台保活服务初始化完成');
return true;
} catch (error) {
console.error('[BackgroundService] 初始化失败:', error);
return false;
}
}
/**
* 停止后台保活服务
*/
export async function stopBackgroundService(): Promise<void> {
try {
await unregisterBackgroundTasks();
if (appStateSubscription) {
appStateSubscription.remove();
appStateSubscription = null;
}
isInitialized = false;
console.log('[BackgroundService] 后台保活服务已停止');
} catch (error) {
console.error('[BackgroundService] 停止服务失败:', error);
}
}
/**
* 检查后台服务状态
*/
export async function checkBackgroundStatus(): Promise<{
isAvailable: boolean;
isInitialized: boolean;
registeredTasks: string[];
}> {
const status = await BackgroundFetch.getStatusAsync();
const registeredTasks = await TaskManager.getRegisteredTasksAsync();
return {
isAvailable: status === BackgroundFetch.BackgroundFetchStatus.Available,
isInitialized,
registeredTasks: registeredTasks.map(task => task.taskName),
};
}
// 导出服务实例
export const backgroundService = {
init: initBackgroundService,
stop: stopBackgroundService,
vibrate: triggerVibration,
vibrateOnMessage,
setVibrationConfig,
getVibrationConfig,
setVibrationEnabled,
checkStatus: checkBackgroundStatus,
};
export default backgroundService;

View File

@@ -0,0 +1,246 @@
/**
* 评论服务
* 处理评论的增删改查、点赞等功能
*/
import { api, PaginatedData } from './api';
import { Comment, CreateCommentInput } from '../types';
// 评论列表响应
interface CommentListResponse {
list: Comment[];
total: number;
page: number;
page_size: number;
total_pages: number;
}
// 评论响应
type CommentResponse = Comment;
// 创建评论请求
interface CreateCommentRequest {
postId: string;
content: string;
parentId?: string;
images?: string[]; // 图片URL列表
replyToUserId?: string;
}
// 更新评论请求
interface UpdateCommentRequest {
content: string;
}
// 评论服务类
class CommentService {
// 获取帖子的评论列表
async getPostComments(
postId: string,
page = 1,
pageSize = 20,
sort: 'latest' | 'hot' = 'latest'
): Promise<PaginatedData<Comment>> {
try {
const response = await api.get<CommentListResponse>(`/comments/post/${postId}`, {
page,
page_size: pageSize,
sort,
});
return response.data;
} catch (error) {
console.error('获取评论列表失败:', error);
return {
list: [],
total: 0,
page: 1,
page_size: pageSize,
total_pages: 0,
};
}
}
// 获取评论的回复列表
async getCommentReplies(
commentId: string,
page = 1,
pageSize = 20
): Promise<PaginatedData<Comment>> {
try {
const response = await api.get<CommentListResponse>(`/comments/${commentId}/replies`, {
page,
page_size: pageSize,
});
return response.data;
} catch (error) {
console.error('获取评论回复列表失败:', error);
return {
list: [],
total: 0,
page: 1,
page_size: pageSize,
total_pages: 0,
};
}
}
// 根据根评论ID分页获取回复扁平化
async getFlatReplies(
rootId: string,
page = 1,
pageSize = 10
): Promise<PaginatedData<Comment>> {
try {
const response = await api.get<CommentListResponse>(`/comments/${rootId}/replies/flat`, {
page,
page_size: pageSize,
});
return response.data;
} catch (error) {
console.error('获取扁平化回复列表失败:', error);
return {
list: [],
total: 0,
page: 1,
page_size: pageSize,
total_pages: 0,
};
}
}
// 获取评论详情
async getComment(commentId: string): Promise<Comment | null> {
try {
const response = await api.get<CommentResponse>(`/comments/${commentId}`);
return response.data;
} catch (error) {
console.error('获取评论详情失败:', error);
return null;
}
}
// 创建评论
async createComment(data: CreateCommentRequest): Promise<Comment | null> {
try {
// 转换字段名为后端期望的snake_case
// 只有当 parentId 有值时才包含 parent_id 字段
const requestBody: Record<string, any> = {
post_id: data.postId,
content: data.content,
};
// 如果有父评论ID才添加 parent_id 字段
if (data.parentId) {
requestBody.parent_id = data.parentId;
}
// 如果有图片,添加 images 字段
if (data.images && data.images.length > 0) {
requestBody.images = data.images;
}
const response = await api.post<CommentResponse>('/comments', requestBody);
return response.data;
} catch (error) {
console.error('创建评论失败:', error);
return null;
}
}
// 回复评论
async replyComment(
postId: string,
parentId: string,
content: string,
replyToUserId?: string
): Promise<Comment | null> {
return this.createComment({
postId,
content,
parentId,
replyToUserId,
});
}
// 更新评论
async updateComment(commentId: string, content: string): Promise<Comment | null> {
try {
const response = await api.put<CommentResponse>(`/comments/${commentId}`, { content });
return response.data;
} catch (error) {
console.error('更新评论失败:', error);
return null;
}
}
// 删除评论
async deleteComment(commentId: string): Promise<boolean> {
try {
const response = await api.delete(`/comments/${commentId}`);
return response.code === 0;
} catch (error) {
console.error('删除评论失败:', error);
return false;
}
}
// 点赞评论
async likeComment(commentId: string): Promise<boolean> {
try {
const response = await api.post(`/comments/${commentId}/like`);
return response.code === 0;
} catch (error) {
console.error('点赞评论失败:', error);
return false;
}
}
// 取消点赞评论
async unlikeComment(commentId: string): Promise<boolean> {
try {
const response = await api.delete(`/comments/${commentId}/like`);
return response.code === 0;
} catch (error) {
console.error('取消点赞评论失败:', error);
return false;
}
}
// 获取用户评论列表
async getUserComments(
userId: string,
page = 1,
pageSize = 20
): Promise<PaginatedData<Comment>> {
try {
const response = await api.get<CommentListResponse>(`/users/${userId}/comments`, {
page,
page_size: pageSize,
});
return response.data;
} catch (error) {
console.error('获取用户评论列表失败:', error);
return {
list: [],
total: 0,
page: 1,
page_size: pageSize,
total_pages: 0,
};
}
}
// 举报评论
async reportComment(commentId: string, reason: string): Promise<boolean> {
try {
const response = await api.post(`/comments/${commentId}/report`, { reason });
return response.code === 0;
} catch (error) {
console.error('举报评论失败:', error);
return false;
}
}
}
// 导出评论服务实例
export const commentService = new CommentService();

935
src/services/database.ts Normal file
View File

@@ -0,0 +1,935 @@
/**
* 本地数据库服务
* 使用 SQLite 存储聊天记录
* 每个用户使用独立的数据库文件,避免数据串用
*/
import * as SQLite from 'expo-sqlite';
import type {
UserDTO,
GroupResponse,
GroupMemberResponse,
ConversationResponse,
ConversationDetailResponse,
} from '../types/dto';
// 数据库实例
let db: SQLite.SQLiteDatabase | null = null;
let writeQueue: Promise<void> = Promise.resolve();
// 当前数据库对应的用户ID
let currentDbUserId: string | null = null;
// 数据库版本,用于迁移
const DB_VERSION = 2;
/**
* 初始化用户数据库
* @param userId 用户ID用于创建用户专属数据库文件
*/
export const initDatabase = async (userId?: string): Promise<void> => {
try {
// 如果指定了用户ID使用用户专属数据库
// 否则使用默认数据库(兼容旧版本)
const dbName = userId ? `carrot_bbs_${userId}.db` : 'carrot_bbs.db';
// 如果存在旧的数据库连接且用户ID变化需要关闭旧连接
if (db && currentDbUserId !== userId) {
console.log(`[Database] 切换用户数据库: ${currentDbUserId} -> ${userId}`);
await closeDatabase();
}
// 如果已经打开了正确的数据库,直接返回
if (db && currentDbUserId === userId) {
console.log(`[Database] 数据库已初始化: ${dbName}`);
return;
}
db = await SQLite.openDatabaseAsync(dbName);
currentDbUserId = userId || null;
console.log(`[Database] 打开数据库: ${dbName}`);
// 创建消息表(包含新字段 seq, status, segments
await db.execAsync(`
CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY NOT NULL,
conversationId TEXT NOT NULL,
senderId TEXT NOT NULL,
content TEXT,
type TEXT DEFAULT 'text',
isRead INTEGER DEFAULT 0,
createdAt TEXT NOT NULL,
seq INTEGER DEFAULT 0,
status TEXT DEFAULT 'normal',
segments TEXT
);
`);
// 创建会话表
await db.execAsync(`
CREATE TABLE IF NOT EXISTS conversations (
id TEXT PRIMARY KEY NOT NULL,
participantId TEXT NOT NULL,
lastMessageId TEXT,
lastSeq INTEGER DEFAULT 0,
unreadCount INTEGER DEFAULT 0,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
`);
// 会话缓存表(完整 JSON兼容私聊/群聊不同结构)
await db.execAsync(`
CREATE TABLE IF NOT EXISTS conversation_cache (
id TEXT PRIMARY KEY NOT NULL,
data TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
`);
// 会话列表缓存表(仅用于 ChatList避免被详情缓存污染
await db.execAsync(`
CREATE TABLE IF NOT EXISTS conversation_list_cache (
id TEXT PRIMARY KEY NOT NULL,
data TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
`);
// 用户缓存表
await db.execAsync(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY NOT NULL,
data TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
`);
// 当前登录用户缓存(仅保存 me
await db.execAsync(`
CREATE TABLE IF NOT EXISTS current_user_cache (
id TEXT PRIMARY KEY NOT NULL,
data TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
`);
// 群组缓存表
await db.execAsync(`
CREATE TABLE IF NOT EXISTS groups (
id TEXT PRIMARY KEY NOT NULL,
data TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
`);
// 群成员缓存表
await db.execAsync(`
CREATE TABLE IF NOT EXISTS group_members (
groupId TEXT NOT NULL,
userId TEXT NOT NULL,
data TEXT NOT NULL,
updatedAt TEXT NOT NULL,
PRIMARY KEY (groupId, userId)
);
`);
// 创建索引
await db.execAsync(`
CREATE INDEX IF NOT EXISTS idx_messages_conversationId ON messages(conversationId);
`);
await db.execAsync(`
CREATE INDEX IF NOT EXISTS idx_group_members_groupId ON group_members(groupId);
`);
// 迁移:检查并添加新列(兼容旧版本数据库)
await migrateDatabase();
// 迁移后再创建依赖新列的索引
await db.execAsync(`
CREATE INDEX IF NOT EXISTS idx_messages_seq ON messages(seq);
`);
await db.execAsync(`
CREATE INDEX IF NOT EXISTS idx_messages_conversation_seq ON messages(conversationId, seq);
`);
console.log('数据库初始化成功');
} catch (error) {
console.error('数据库初始化失败:', error);
throw error;
}
};
/**
* 关闭数据库连接
*/
export const closeDatabase = async (): Promise<void> => {
if (db) {
try {
await db.closeAsync();
console.log('[Database] 数据库连接已关闭');
} catch (error) {
console.error('[Database] 关闭数据库失败:', error);
}
db = null;
currentDbUserId = null;
}
};
/**
* 获取当前数据库用户ID
*/
export const getCurrentDbUserId = (): string | null => {
return currentDbUserId;
};
// 数据库迁移
const migrateDatabase = async (): Promise<void> => {
const database = await getDb();
try {
// 检查 messages 表是否有 seq 列
const tableInfo = await database.getAllAsync<any>(
`PRAGMA table_info(messages)`
);
const columns = tableInfo.map(col => col.name);
// 添加 seq 列
if (!columns.includes('seq')) {
await database.execAsync(`ALTER TABLE messages ADD COLUMN seq INTEGER DEFAULT 0`);
console.log('数据库迁移:添加 seq 列');
}
// 添加 status 列
if (!columns.includes('status')) {
await database.execAsync(`ALTER TABLE messages ADD COLUMN status TEXT DEFAULT 'normal'`);
console.log('数据库迁移:添加 status 列');
}
// 添加 segments 列(用于存储消息的 segments JSON
if (!columns.includes('segments')) {
await database.execAsync(`ALTER TABLE messages ADD COLUMN segments TEXT`);
console.log('数据库迁移:添加 segments 列');
}
// 检查 conversations 表是否有 lastSeq 列
const convTableInfo = await database.getAllAsync<any>(
`PRAGMA table_info(conversations)`
);
const convColumns = convTableInfo.map(col => col.name);
if (!convColumns.includes('lastSeq')) {
await database.execAsync(`ALTER TABLE conversations ADD COLUMN lastSeq INTEGER DEFAULT 0`);
console.log('数据库迁移:添加 lastSeq 列');
}
} catch (error) {
console.error('数据库迁移失败:', error);
}
};
// 获取数据库实例
const getDb = async (): Promise<SQLite.SQLiteDatabase> => {
if (!db) {
throw new Error('数据库未初始化,请先调用 initDatabase(userId)');
}
return db;
};
const isRecoverableDbError = (error: unknown): boolean => {
const message = String(error);
return (
message.includes('NativeDatabase.prepareAsync') ||
message.includes('NullPointerException')
);
};
const withDbRead = async <T>(operation: (database: SQLite.SQLiteDatabase) => Promise<T>): Promise<T> => {
let database = await getDb();
try {
return await operation(database);
} catch (error) {
if (!isRecoverableDbError(error)) {
throw error;
}
console.error('数据库读取异常,尝试重连后重试:', error);
db = null;
database = await getDb();
return operation(database);
}
};
const enqueueWrite = async <T>(operation: () => Promise<T>): Promise<T> => {
const wrappedOperation = async () => {
try {
return await operation();
} catch (error) {
if (!isRecoverableDbError(error)) {
throw error;
}
console.error('数据库写入异常,尝试重连后重试:', error);
db = null;
return operation();
}
};
const queued = writeQueue.then(wrappedOperation, wrappedOperation);
writeQueue = queued.then(() => undefined, () => undefined);
return queued;
};
// ==================== 消息类型定义 ====================
export interface CachedMessage {
id: string;
conversationId: string;
senderId: string;
content?: string;
type?: string;
isRead: boolean;
createdAt: string;
seq: number;
status: string;
segments?: any;
}
// ==================== 消息操作 ====================
// 保存单条消息
export const saveMessage = async (message: CachedMessage): Promise<void> => {
await enqueueWrite(async () => {
const database = await getDb();
await database.runAsync(
`INSERT OR REPLACE INTO messages (id, conversationId, senderId, content, type, isRead, createdAt, seq, status, segments)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
message.id,
message.conversationId,
message.senderId,
message.content || '',
message.type || 'text',
message.isRead ? 1 : 0,
message.createdAt,
message.seq || 0,
message.status || 'normal',
message.segments ? JSON.stringify(message.segments) : null,
]
);
});
};
// 批量保存消息
export const saveMessagesBatch = async (messages: CachedMessage[]): Promise<void> => {
if (!messages || messages.length === 0) return;
await enqueueWrite(async () => {
const database = await getDb();
for (const message of messages) {
await database.runAsync(
`INSERT OR REPLACE INTO messages (id, conversationId, senderId, content, type, isRead, createdAt, seq, status, segments)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
message.id,
message.conversationId,
message.senderId,
message.content || '',
message.type || 'text',
message.isRead ? 1 : 0,
message.createdAt,
message.seq || 0,
message.status || 'normal',
message.segments ? JSON.stringify(message.segments) : null,
]
);
}
});
};
// 获取会话的消息默认只加载最近的20条避免卡顿
export const getMessagesByConversation = async (
conversationId: string,
limit: number = 20
): Promise<CachedMessage[]> => {
return withDbRead(async (database) => {
const result = await database.getAllAsync<any>(
`SELECT * FROM messages
WHERE conversationId = ?
ORDER BY seq DESC
LIMIT ?`,
[conversationId, limit]
);
// 反转以保持时间正序(最新的在底部)
return result.reverse().map(msg => ({
...msg,
isRead: msg.isRead === 1,
segments: msg.segments ? JSON.parse(msg.segments) : undefined,
}));
});
};
// 获取会话的最大消息序号
export const getMaxSeq = async (conversationId: string): Promise<number> => {
return withDbRead(async (database) => {
const result = await database.getFirstAsync<{ maxSeq: number }>(
`SELECT MAX(seq) as maxSeq FROM messages WHERE conversationId = ?`,
[conversationId]
);
return result?.maxSeq || 0;
});
};
// 获取会话的最小消息序号
export const getMinSeq = async (conversationId: string): Promise<number> => {
return withDbRead(async (database) => {
const result = await database.getFirstAsync<{ minSeq: number }>(
`SELECT MIN(seq) as minSeq FROM messages WHERE conversationId = ?`,
[conversationId]
);
return result?.minSeq || 0;
});
};
// 获取指定 seq 之前的消息(用于加载历史)
export const getMessagesBeforeSeq = async (conversationId: string, beforeSeq: number, limit: number = 20): Promise<CachedMessage[]> => {
return withDbRead(async (database) => {
const result = await database.getAllAsync<any>(
`SELECT * FROM messages
WHERE conversationId = ? AND seq < ?
ORDER BY seq DESC
LIMIT ?`,
[conversationId, beforeSeq, limit]
);
return result.map(msg => ({
...msg,
isRead: msg.isRead === 1,
segments: msg.segments ? JSON.parse(msg.segments) : undefined,
})).reverse(); // 反转以保持时间正序
});
};
// 获取本地消息数量(用于判断是否还有更多历史)
export const getLocalMessageCountBeforeSeq = async (conversationId: string, beforeSeq: number): Promise<number> => {
const database = await getDb();
const result = await database.getFirstAsync<{ count: number }>(
`SELECT COUNT(*) as count FROM messages WHERE conversationId = ? AND seq < ?`,
[conversationId, beforeSeq]
);
return result?.count || 0;
};
// 获取消息数量
export const getMessageCount = async (conversationId: string): Promise<number> => {
const database = await getDb();
const result = await database.getFirstAsync<{ count: number }>(
`SELECT COUNT(*) as count FROM messages WHERE conversationId = ?`,
[conversationId]
);
return result?.count || 0;
};
// 标记消息为已读
export const markMessageAsRead = async (messageId: string): Promise<void> => {
await enqueueWrite(async () => {
const database = await getDb();
await database.runAsync(
`UPDATE messages SET isRead = 1 WHERE id = ?`,
[messageId]
);
});
};
// 标记会话的所有消息为已读
export const markConversationAsRead = async (conversationId: string): Promise<void> => {
await enqueueWrite(async () => {
const database = await getDb();
await database.runAsync(
`UPDATE messages SET isRead = 1 WHERE conversationId = ?`,
[conversationId]
);
});
};
// 删除单条消息
export const deleteMessage = async (messageId: string): Promise<void> => {
await enqueueWrite(async () => {
const database = await getDb();
await database.runAsync(
`DELETE FROM messages WHERE id = ?`,
[messageId]
);
});
};
// 更新消息状态(如撤回)
export const updateMessageStatus = async (messageId: string, status: string): Promise<void> => {
await enqueueWrite(async () => {
const database = await getDb();
await database.runAsync(
`UPDATE messages SET status = ? WHERE id = ?`,
[status, messageId]
);
});
};
// 清空会话的所有消息
export const clearConversationMessages = async (conversationId: string): Promise<void> => {
await enqueueWrite(async () => {
const database = await getDb();
await database.runAsync(
`DELETE FROM messages WHERE conversationId = ?`,
[conversationId]
);
});
};
// ==================== 会话操作 ====================
// 保存或更新会话
export const saveConversation = async (conversation: {
id: string;
participantId: string;
lastMessageId?: string;
lastSeq?: number;
unreadCount?: number;
createdAt: string;
updatedAt: string;
}): Promise<void> => {
await enqueueWrite(async () => {
const database = await getDb();
await database.runAsync(
`INSERT OR REPLACE INTO conversations (id, participantId, lastMessageId, lastSeq, unreadCount, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[
conversation.id,
conversation.participantId,
conversation.lastMessageId || null,
conversation.lastSeq || 0,
conversation.unreadCount || 0,
conversation.createdAt,
conversation.updatedAt,
]
);
});
};
// 获取所有会话
export const getAllConversations = async (): Promise<any[]> => {
const database = await getDb();
const result = await database.getAllAsync<any>(
`SELECT * FROM conversations ORDER BY updatedAt DESC`
);
return result;
};
// 更新会话未读数
export const updateConversationUnreadCount = async (conversationId: string, count: number): Promise<void> => {
await enqueueWrite(async () => {
const database = await getDb();
await database.runAsync(
`UPDATE conversations SET unreadCount = ? WHERE id = ?`,
[count, conversationId]
);
});
};
// 更新会话最后消息
export const updateConversationLastMessage = async (
conversationId: string,
lastMessageId: string,
lastSeq: number,
updatedAt: string
): Promise<void> => {
await enqueueWrite(async () => {
const database = await getDb();
await database.runAsync(
`UPDATE conversations SET lastMessageId = ?, lastSeq = ?, updatedAt = ? WHERE id = ?`,
[lastMessageId, lastSeq, updatedAt, conversationId]
);
});
};
// 删除会话(包含所有消息)
export const deleteConversation = async (conversationId: string): Promise<void> => {
await enqueueWrite(async () => {
const database = await getDb();
await database.runAsync(
`DELETE FROM messages WHERE conversationId = ?`,
[conversationId]
);
await database.runAsync(
`DELETE FROM conversations WHERE id = ?`,
[conversationId]
);
});
};
// ==================== 搜索 ====================
// 搜索消息
export const searchMessages = async (keyword: string): Promise<CachedMessage[]> => {
const database = await getDb();
const result = await database.getAllAsync<any>(
`SELECT * FROM messages
WHERE content LIKE ?
ORDER BY createdAt DESC
LIMIT 50`,
[`%${keyword}%`]
);
return result.map(msg => ({
...msg,
isRead: msg.isRead === 1,
segments: msg.segments ? JSON.parse(msg.segments) : undefined,
}));
};
// ==================== 工具函数 ====================
// 获取数据库统计信息
export const getDatabaseStats = async (): Promise<{
totalMessages: number;
totalConversations: number;
}> => {
const database = await getDb();
const msgCount = await database.getFirstAsync<{ count: number }>(
`SELECT COUNT(*) as count FROM messages`
);
const convCount = await database.getFirstAsync<{ count: number }>(
`SELECT COUNT(*) as count FROM conversations`
);
return {
totalMessages: msgCount?.count || 0,
totalConversations: convCount?.count || 0,
};
};
// 清空所有数据(用于调试或用户退出登录)
export const clearAllData = async (): Promise<void> => {
await enqueueWrite(async () => {
const database = await getDb();
await database.execAsync(`DELETE FROM messages`);
await database.execAsync(`DELETE FROM conversations`);
await database.execAsync(`DELETE FROM conversation_cache`);
await database.execAsync(`DELETE FROM conversation_list_cache`);
await database.execAsync(`DELETE FROM users`);
await database.execAsync(`DELETE FROM current_user_cache`);
await database.execAsync(`DELETE FROM groups`);
await database.execAsync(`DELETE FROM group_members`);
});
};
// ==================== 通用 JSON 缓存辅助 ====================
const safeParseJson = <T>(value: string): T | null => {
try {
return JSON.parse(value) as T;
} catch {
return null;
}
};
// ==================== 用户缓存 ====================
export const saveUserCache = async (user: UserDTO): Promise<void> => {
await enqueueWrite(async () => {
const database = await getDb();
await database.runAsync(
`INSERT OR REPLACE INTO users (id, data, updatedAt) VALUES (?, ?, ?)`,
[String(user.id), JSON.stringify(user), new Date().toISOString()]
);
});
};
export const saveUsersCache = async (users: UserDTO[]): Promise<void> => {
if (!users || users.length === 0) return;
await enqueueWrite(async () => {
const database = await getDb();
for (const user of users) {
await database.runAsync(
`INSERT OR REPLACE INTO users (id, data, updatedAt) VALUES (?, ?, ?)`,
[String(user.id), JSON.stringify(user), new Date().toISOString()]
);
}
});
};
export const getUserCache = async (userId: string): Promise<UserDTO | null> => {
return withDbRead(async (database) => {
const row = await database.getFirstAsync<{ data: string }>(
`SELECT data FROM users WHERE id = ?`,
[String(userId)]
);
if (!row?.data) return null;
return safeParseJson<UserDTO>(row.data);
});
};
export const getLatestUserCache = async (): Promise<UserDTO | null> => {
const database = await getDb();
const row = await database.getFirstAsync<{ data: string }>(
`SELECT data FROM users ORDER BY updatedAt DESC LIMIT 1`
);
if (!row?.data) return null;
return safeParseJson<UserDTO>(row.data);
};
export const saveCurrentUserCache = async (user: UserDTO): Promise<void> => {
await enqueueWrite(async () => {
const database = await getDb();
await database.runAsync(
`INSERT OR REPLACE INTO current_user_cache (id, data, updatedAt) VALUES (?, ?, ?)`,
['me', JSON.stringify(user), new Date().toISOString()]
);
});
};
export const getCurrentUserCache = async (): Promise<UserDTO | null> => {
return withDbRead(async (database) => {
const row = await database.getFirstAsync<{ data: string }>(
`SELECT data FROM current_user_cache WHERE id = 'me'`
);
if (!row?.data) return null;
return safeParseJson<UserDTO>(row.data);
});
};
export const clearCurrentUserCache = async (): Promise<void> => {
await enqueueWrite(async () => {
const database = await getDb();
await database.runAsync(`DELETE FROM current_user_cache WHERE id = 'me'`);
});
};
// ==================== 群组缓存 ====================
export const saveGroupCache = async (group: GroupResponse): Promise<void> => {
await enqueueWrite(async () => {
const database = await getDb();
await database.runAsync(
`INSERT OR REPLACE INTO groups (id, data, updatedAt) VALUES (?, ?, ?)`,
[String(group.id), JSON.stringify(group), new Date().toISOString()]
);
});
};
export const saveGroupsCache = async (groups: GroupResponse[]): Promise<void> => {
if (!groups || groups.length === 0) return;
await enqueueWrite(async () => {
const database = await getDb();
for (const group of groups) {
await database.runAsync(
`INSERT OR REPLACE INTO groups (id, data, updatedAt) VALUES (?, ?, ?)`,
[String(group.id), JSON.stringify(group), new Date().toISOString()]
);
}
});
};
export const getGroupCache = async (groupId: string): Promise<GroupResponse | null> => {
return withDbRead(async (database) => {
const row = await database.getFirstAsync<{ data: string }>(
`SELECT data FROM groups WHERE id = ?`,
[String(groupId)]
);
if (!row?.data) return null;
return safeParseJson<GroupResponse>(row.data);
});
};
export const getAllGroupsCache = async (): Promise<GroupResponse[]> => {
return withDbRead(async (database) => {
const rows = await database.getAllAsync<{ data: string }>(
`SELECT data FROM groups ORDER BY updatedAt DESC`
);
return rows
.map(row => safeParseJson<GroupResponse>(row.data))
.filter((item): item is GroupResponse => Boolean(item));
});
};
// ==================== 群成员缓存 ====================
export const saveGroupMembersCache = async (
groupId: string,
members: GroupMemberResponse[]
): Promise<void> => {
await enqueueWrite(async () => {
const database = await getDb();
const now = new Date().toISOString();
await database.runAsync(`DELETE FROM group_members WHERE groupId = ?`, [String(groupId)]);
for (const member of members) {
await database.runAsync(
`INSERT OR REPLACE INTO group_members (groupId, userId, data, updatedAt) VALUES (?, ?, ?, ?)`,
[String(groupId), String(member.user_id), JSON.stringify(member), now]
);
}
});
};
export const getGroupMembersCache = async (groupId: string): Promise<GroupMemberResponse[]> => {
return withDbRead(async (database) => {
const rows = await database.getAllAsync<{ data: string }>(
`SELECT data FROM group_members WHERE groupId = ? ORDER BY updatedAt DESC`,
[String(groupId)]
);
return rows
.map(row => safeParseJson<GroupMemberResponse>(row.data))
.filter((item): item is GroupMemberResponse => Boolean(item));
});
};
// ==================== 会话缓存 ====================
type ConversationCacheData = ConversationResponse | ConversationDetailResponse;
export const saveConversationCache = async (conversation: ConversationCacheData): Promise<void> => {
await enqueueWrite(async () => {
const database = await getDb();
const cacheUpdatedAt = ('updated_at' in conversation && conversation.updated_at)
? String(conversation.updated_at)
: new Date().toISOString();
await database.runAsync(
`INSERT OR REPLACE INTO conversation_cache (id, data, updatedAt) VALUES (?, ?, ?)`,
[String(conversation.id), JSON.stringify(conversation), cacheUpdatedAt]
);
});
};
export const saveConversationsCache = async (conversations: ConversationResponse[]): Promise<void> => {
if (!conversations || conversations.length === 0) return;
await enqueueWrite(async () => {
const database = await getDb();
for (const conversation of conversations) {
await database.runAsync(
`INSERT OR REPLACE INTO conversation_list_cache (id, data, updatedAt) VALUES (?, ?, ?)`,
[
String(conversation.id),
JSON.stringify(conversation),
conversation.updated_at || new Date().toISOString(),
]
);
}
});
};
export const getConversationCache = async (conversationId: string): Promise<ConversationCacheData | null> => {
return withDbRead(async (database) => {
const row = await database.getFirstAsync<{ data: string }>(
`SELECT data FROM conversation_cache WHERE id = ?`,
[String(conversationId)]
);
if (!row?.data) return null;
return safeParseJson<ConversationCacheData>(row.data);
});
};
export const getConversationListCache = async (): Promise<ConversationResponse[]> => {
return withDbRead(async (database) => {
let rows = await database.getAllAsync<{ data: string }>(
`SELECT data FROM conversation_list_cache ORDER BY updatedAt DESC`
);
// 兼容旧版本:首次升级时尝试从旧表读取一次
if (rows.length === 0) {
rows = await database.getAllAsync<{ data: string }>(
`SELECT data FROM conversation_cache ORDER BY updatedAt DESC`
);
}
const list = rows
.map(row => safeParseJson<ConversationResponse>(row.data))
.filter((item): item is ConversationResponse => Boolean(item))
.filter(item => typeof item.id !== 'undefined' && typeof item.type !== 'undefined');
return list.sort((a, b) => {
const aPinned = a.is_pinned ? 1 : 0;
const bPinned = b.is_pinned ? 1 : 0;
if (aPinned !== bPinned) {
return bPinned - aPinned;
}
const aTime = new Date(a.last_message_at || a.updated_at || 0).getTime();
const bTime = new Date(b.last_message_at || b.updated_at || 0).getTime();
return bTime - aTime;
});
});
};
/**
* 局部更新 conversation_list_cache 中的某条会话(如 last_message、last_message_at、unread_count 等)
* 用于:发送消息后立即更新缓存中的最后一条消息,避免 SWR 返回旧数据
*/
export const updateConversationListCacheEntry = async (
conversationId: string,
updates: Partial<ConversationResponse>
): Promise<void> => {
await enqueueWrite(async () => {
const database = await getDb();
const row = await database.getFirstAsync<{ data: string }>(
`SELECT data FROM conversation_list_cache WHERE id = ?`,
[String(conversationId)]
);
if (!row?.data) return;
const conv = safeParseJson<ConversationResponse>(row.data);
if (!conv) return;
const updated = { ...conv, ...updates };
const updatedAt = updates.last_message_at || updated.last_message_at || new Date().toISOString();
await database.runAsync(
`UPDATE conversation_list_cache SET data = ?, updatedAt = ? WHERE id = ?`,
[JSON.stringify(updated), updatedAt, String(conversationId)]
);
});
};
/**
* 更新会话 SWR 缓存中的未读数(标记已读后立即同步清零,避免下次从缓存加载时仍显示旧红点)
*/
export const updateConversationCacheUnreadCount = async (
conversationId: string,
count: number
): Promise<void> => {
await enqueueWrite(async () => {
const database = await getDb();
// 更新 conversation_list_cache
const listRow = await database.getFirstAsync<{ data: string }>(
`SELECT data FROM conversation_list_cache WHERE id = ?`,
[String(conversationId)]
);
if (listRow?.data) {
const conv = safeParseJson<ConversationResponse>(listRow.data);
if (conv) {
conv.unread_count = count;
await database.runAsync(
`UPDATE conversation_list_cache SET data = ? WHERE id = ?`,
[JSON.stringify(conv), String(conversationId)]
);
}
}
// 更新 conversation_cache
const cacheRow = await database.getFirstAsync<{ data: string }>(
`SELECT data FROM conversation_cache WHERE id = ?`,
[String(conversationId)]
);
if (cacheRow?.data) {
const conv = safeParseJson<Record<string, unknown>>(cacheRow.data);
if (conv) {
conv.unread_count = count;
await database.runAsync(
`UPDATE conversation_cache SET data = ? WHERE id = ?`,
[JSON.stringify(conv), String(conversationId)]
);
}
}
});
};

View File

@@ -0,0 +1,55 @@
import type { AlertButton, AlertOptions } from 'react-native';
export interface DialogPayload {
title?: string;
message?: string;
actions: AlertButton[];
options?: AlertOptions;
}
type DialogListener = (payload: DialogPayload) => void;
let dialogListener: DialogListener | null = null;
export const bindDialogListener = (listener: DialogListener) => {
dialogListener = listener;
return () => {
if (dialogListener === listener) {
dialogListener = null;
}
};
};
export const showDialog = (payload: DialogPayload) => {
dialogListener?.(payload);
};
export interface ConfirmOptions {
title: string;
message?: string;
confirmText?: string;
cancelText?: string;
destructive?: boolean;
onConfirm?: () => void;
onCancel?: () => void;
}
export const showConfirm = (options: ConfirmOptions) => {
showDialog({
title: options.title,
message: options.message,
actions: [
{
text: options.cancelText ?? '取消',
style: 'cancel',
onPress: options.onCancel,
},
{
text: options.confirmText ?? '确认',
style: options.destructive ? 'destructive' : 'default',
onPress: options.onConfirm,
},
],
options: { cancelable: true },
});
};

View File

@@ -0,0 +1,391 @@
/**
* 群组服务
* 处理群组管理、成员管理、群设置、群公告等功能
* 使用新的 API 接口: /api/v1/groups (RESTful action 风格)
*/
import { api } from './api';
import {
GroupResponse,
GroupMemberResponse,
GroupAnnouncementResponse,
GroupListResponse,
GroupMemberListResponse,
GroupAnnouncementListResponse,
CreateGroupRequest,
InviteMembersRequest,
TransferOwnerRequest,
SetRoleRequest,
SetNicknameRequest,
SetMuteAllRequest,
SetJoinTypeRequest,
CreateAnnouncementRequest,
MyMemberInfoResponse,
SetGroupAvatarRequest,
HandleGroupRequestAction,
} from '../types/dto';
// 群组服务类(纯 API 层)
// SQLite/内存缓存编排由 groupManager 统一处理
class GroupService {
async fetchGroupsFromApi(page = 1, pageSize = 20): Promise<GroupListResponse> {
const response = await api.get<GroupListResponse>('/groups/list', {
page,
page_size: pageSize,
});
return response.data;
}
async fetchGroupFromApi(id: number | string): Promise<GroupResponse> {
const response = await api.get<GroupResponse>('/groups/get', {
group_id: String(id),
});
return response.data;
}
async fetchMembersFromApi(
groupId: number | string,
page = 1,
pageSize = 50
): Promise<GroupMemberListResponse> {
const response = await api.get<GroupMemberListResponse>(
'/groups/get_members',
{
group_id: String(groupId),
page,
page_size: pageSize,
}
);
return response.data;
}
// ==================== 群组管理 ====================
/**
* 创建群组
* POST /api/v1/groups/create
*/
async createGroup(data: CreateGroupRequest): Promise<GroupResponse> {
const response = await api.post<GroupResponse>('/groups/create', data);
return response.data;
}
/**
* 获取群组列表
* GET /api/v1/groups/list
*/
async getGroups(page = 1, pageSize = 20): Promise<GroupListResponse> {
return this.fetchGroupsFromApi(page, pageSize);
}
/**
* 获取群组详情
* GET /api/v1/groups/get?group_id=xxx
*/
async getGroup(id: number | string): Promise<GroupResponse> {
return this.fetchGroupFromApi(id);
}
/**
* 获取当前用户在群组中的成员信息
* GET /api/v1/groups/get_my_info?group_id=xxx
*/
async getMyMemberInfo(groupId: number | string): Promise<MyMemberInfoResponse> {
const response = await api.get<MyMemberInfoResponse>('/groups/get_my_info', {
group_id: String(groupId),
});
return response.data;
}
/**
* 解散群组
* POST /api/v1/groups/dissolve
* Body: { "group_id": "xxx" }
*/
async dissolveGroup(id: number | string): Promise<void> {
await api.post('/groups/dissolve', {
group_id: String(id),
});
}
/**
* 转让群主
* POST /api/v1/groups/transfer
* Body: { "group_id": "xxx", "new_owner_id": "xxx" }
*/
async transferOwner(id: number | string, data: TransferOwnerRequest): Promise<void> {
await api.post('/groups/transfer', {
group_id: String(id),
...data,
});
}
// ==================== 成员管理 ====================
/**
* 邀请成员加入群组
* POST /api/v1/groups/invite_members
* Body: { "group_id": "xxx", "user_ids": [...] }
*/
async inviteMembers(groupId: number | string, data: InviteMembersRequest): Promise<void> {
await api.post('/groups/invite_members', {
group_id: String(groupId),
...data,
});
}
/**
* 申请加入群组
* POST /api/v1/groups/join
* Body: { "group_id": "xxx" }
*/
async joinGroup(groupId: number | string): Promise<void> {
await api.post('/groups/join', {
group_id: String(groupId),
});
}
/**
* 响应群邀请
* POST /api/v1/groups/respond_invite
*/
async respondInvite(data: HandleGroupRequestAction): Promise<void> {
await api.post('/groups/respond_invite', data);
}
/**
* 审批主动加群申请
* POST /api/v1/groups/set_group_add_request
*/
async reviewJoinRequest(data: HandleGroupRequestAction): Promise<void> {
await api.post('/groups/set_group_add_request', data);
}
/**
* 退出群组
* POST /api/v1/groups/set_group_leave
* Body: { "group_id": "xxx" }
*/
async leaveGroup(groupId: number | string): Promise<void> {
await api.post('/groups/set_group_leave', {
group_id: String(groupId),
});
}
/**
* 获取群组成员列表
* GET /api/v1/groups/get_members?group_id=xxx
*/
async getMembers(groupId: number | string, page = 1, pageSize = 50): Promise<GroupMemberListResponse> {
return this.fetchMembersFromApi(groupId, page, pageSize);
}
/**
* 移除群成员(踢人)
* POST /api/v1/groups/set_group_kick
* Body: { "group_id": "xxx", "user_id": "xxx", "reject_add_request": false }
*/
async removeMember(groupId: number | string, userId: string): Promise<void> {
await api.post('/groups/set_group_kick', {
group_id: String(groupId),
user_id: userId,
reject_add_request: false,
});
}
/**
* 设置成员角色(设置/取消管理员)
* POST /api/v1/groups/set_group_admin
* Body: { "group_id": "xxx", "user_id": "xxx", "enable": true }
*/
async setMemberRole(
groupId: number | string,
userId: string,
data: SetRoleRequest
): Promise<void> {
await api.post('/groups/set_group_admin', {
group_id: String(groupId),
user_id: userId,
enable: data.role === 'admin',
});
}
/**
* 设置群昵称
* POST /api/v1/groups/set_nickname
* Body: { "group_id": "xxx", "nickname": "xxx" }
*/
async setNickname(groupId: number | string, data: SetNicknameRequest): Promise<void> {
await api.post('/groups/set_nickname', {
group_id: String(groupId),
...data,
});
}
/**
* 禁言/解禁成员
* POST /api/v1/groups/set_group_ban
* Body: { "group_id": "xxx", "user_id": "xxx", "duration": 3600 }
* @param groupId 群组ID
* @param userId 用户ID
* @param duration 禁言时长0表示解除禁言
*/
async muteMember(
groupId: number | string,
userId: string,
duration: number = 0
): Promise<void> {
await api.post('/groups/set_group_ban', {
group_id: String(groupId),
user_id: userId,
duration: duration,
});
}
// ==================== 群设置 ====================
/**
* 设置全员禁言
* POST /api/v1/groups/set_group_whole_ban
* Body: { "group_id": "xxx", "enable": true }
*/
async setMuteAll(groupId: number | string, data: SetMuteAllRequest): Promise<void> {
await api.post('/groups/set_group_whole_ban', {
group_id: String(groupId),
enable: data.mute_all,
});
}
/**
* 设置加群方式
* POST /api/v1/groups/set_join_type
* Body: { "group_id": "xxx", "join_type": 1 }
*/
async setJoinType(groupId: number | string, data: SetJoinTypeRequest): Promise<void> {
await api.post('/groups/set_join_type', {
group_id: String(groupId),
...data,
});
}
/**
* 设置群名称
* POST /api/v1/groups/set_group_name
* Body: { "group_id": "xxx", "group_name": "xxx" }
*/
async setGroupName(groupId: number | string, groupName: string): Promise<void> {
await api.post('/groups/set_group_name', {
group_id: String(groupId),
group_name: groupName,
});
}
/**
* 设置群头像
* POST /api/v1/groups/set_group_avatar
* Body: { "group_id": "xxx", "avatar": "xxx" }
*/
async setGroupAvatar(groupId: number | string, avatarUrl: string): Promise<GroupResponse> {
const response = await api.post<GroupResponse>('/groups/set_group_avatar', {
group_id: String(groupId),
avatar: avatarUrl,
});
return response.data;
}
// ==================== 群公告 ====================
/**
* 创建群公告
* POST /api/v1/groups/create_announcement
* Body: { "group_id": "xxx", "content": "xxx" }
*/
async createAnnouncement(
groupId: number | string,
data: CreateAnnouncementRequest
): Promise<GroupAnnouncementResponse> {
const response = await api.post<GroupAnnouncementResponse>(
'/groups/create_announcement',
{
group_id: String(groupId),
...data,
}
);
return response.data;
}
/**
* 获取群公告列表
* GET /api/v1/groups/get_announcements?group_id=xxx
*/
async getAnnouncements(
groupId: number | string,
page = 1,
pageSize = 20
): Promise<GroupAnnouncementListResponse> {
try {
const response = await api.get<GroupAnnouncementListResponse>(
'/groups/get_announcements',
{
group_id: String(groupId),
page,
page_size: pageSize,
}
);
return response.data;
} catch (error) {
console.error('获取群公告列表失败:', error);
return {
list: [],
total: 0,
page: 1,
page_size: pageSize,
total_pages: 0,
};
}
}
/**
* 删除群公告
* POST /api/v1/groups/delete_announcement
* Body: { "group_id": "xxx", "announcement_id": "xxx" }
*/
async deleteAnnouncement(groupId: number | string, announcementId: number): Promise<void> {
await api.post('/groups/delete_announcement', {
group_id: String(groupId),
announcement_id: announcementId,
});
}
// ==================== 兼容旧API的方法将逐步废弃 ====================
/**
* @deprecated 使用 setGroupName 代替
*/
async updateGroup(id: number, data: { name: string }): Promise<GroupResponse> {
await this.setGroupName(id, data.name);
return this.getGroup(id);
}
}
// 导出群组服务实例
export const groupService = new GroupService();
// 导出类型以方便使用
export type {
GroupResponse,
GroupMemberResponse,
GroupAnnouncementResponse,
GroupListResponse,
GroupMemberListResponse,
GroupAnnouncementListResponse,
CreateGroupRequest,
InviteMembersRequest,
TransferOwnerRequest,
SetRoleRequest,
SetNicknameRequest,
SetMuteAllRequest,
SetJoinTypeRequest,
CreateAnnouncementRequest,
SetGroupAvatarRequest,
};

106
src/services/index.ts Normal file
View File

@@ -0,0 +1,106 @@
/**
* 服务导出
* 统一导出所有API服务
*/
// API 客户端
export { api, WS_URL, TOKEN_KEY, REFRESH_TOKEN_KEY } from './api';
export type { ApiResponse, PaginatedData, ApiError } from './api';
// 认证服务
export { authService } from './authService';
export { resolveAuthApiError } from './authService';
export type {
LoginRequest,
RegisterRequest,
SendEmailCodeRequest,
ResetPasswordRequest,
VerifyCurrentUserEmailRequest,
AuthResponse,
RefreshTokenResponse,
UpdateUserRequest,
BlockedUserListResponse,
BlockStatusResponse,
} from './authService';
// 帖子服务
export { postService } from './postService';
// 评论服务
export { commentService } from './commentService';
// 消息服务
export { messageService } from './messageService';
// 通知服务
export { notificationService } from './notificationService';
// 文件上传服务
export { uploadService } from './uploadService';
export type { UploadResponse } from './uploadService';
// 推送服务
export { pushService, registerDevice, getDevices, unregisterDevice, updateDeviceToken, getPushRecords } from './pushService';
// 投票服务
export { voteService } from './voteService';
// WebSocket 服务
export { websocketService } from './websocketService';
export type {
WSMessage,
WSMessageType,
WSChatMessage,
WSReadMessage,
WSTypingMessage,
WSRecallMessage,
WSNotificationMessage,
WSAnnouncementMessage,
WSGroupChatMessage,
WSGroupTypingMessage,
GroupNoticeType,
WSGroupNoticeMessage,
WSGroupMentionMessage,
WSGroupReadMessage,
WSGroupRecallMessage
} from './websocketService';
// 系统通知服务
export { systemNotificationService, getNotificationTitle } from './systemNotificationService';
export type { AppNotificationType } from './systemNotificationService';
// 群组服务
export { groupService } from './groupService';
export type {
GroupResponse,
GroupMemberResponse,
GroupAnnouncementResponse,
GroupListResponse,
GroupMemberListResponse,
GroupAnnouncementListResponse,
CreateGroupRequest,
InviteMembersRequest,
TransferOwnerRequest,
SetRoleRequest,
SetNicknameRequest,
SetMuteAllRequest,
SetJoinTypeRequest,
CreateAnnouncementRequest,
} from './groupService';
// 后台保活服务
export {
backgroundService,
initBackgroundService,
stopBackgroundService,
triggerVibration,
vibrateOnMessage,
setVibrationConfig,
getVibrationConfig,
setVibrationEnabled,
checkBackgroundStatus,
} from './backgroundService';
// 统一提示/弹窗服务
export { showPrompt } from './promptService';
export { showConfirm } from './dialogService';

View File

@@ -0,0 +1,597 @@
/**
* 消息服务
* 处理私信/聊天消息功能
* 使用新的 API 接口: /api/v1/conversations (RESTful action 风格)
*/
import { api } from './api';
import {
ConversationListResponse,
ConversationResponse,
ConversationDetailResponse,
MessageListResponse,
MessageResponse,
SendMessageRequest,
UnreadCountResponse,
ConversationUnreadCountResponse,
SystemMessageListResponse,
SystemUnreadCountResponse,
MessageSegment,
} from '../types/dto';
import {
getConversationCache,
getConversationListCache,
saveConversationCache,
saveConversationsCache,
saveGroupsCache,
saveUsersCache,
updateConversationCacheUnreadCount,
} from './database';
// 消息服务类
class MessageService {
private async refreshConversationsFromServer(
page = 1,
pageSize = 20
): Promise<ConversationListResponse> {
const response = await api.get<ConversationListResponse>('/conversations/list', {
page,
page_size: pageSize,
});
const list = response.data.list || [];
await saveConversationsCache(list);
await saveUsersCache(
list.flatMap(conv => [
...(conv.participants || []),
...(conv.last_message?.sender ? [conv.last_message.sender] : []),
])
);
await saveGroupsCache(
list
.map(conv => conv.group)
.filter((group): group is NonNullable<typeof group> => Boolean(group))
);
return response.data;
}
private async refreshConversationDetailFromServer(id: string): Promise<ConversationDetailResponse> {
const response = await api.get<ConversationDetailResponse>(
'/conversations/get',
{ conversation_id: id }
);
await saveConversationCache(response.data);
if (response.data.participants?.length) {
await saveUsersCache(response.data.participants);
}
if (response.data.last_message?.sender) {
await saveUsersCache([response.data.last_message.sender]);
}
return response.data;
}
private normalizeCachedDetail(
id: string,
cached: any
): ConversationDetailResponse {
return {
id: String(cached?.id || id),
type: cached?.type || 'private',
is_pinned: Boolean(cached?.is_pinned),
last_seq: Number(cached?.last_seq || 0),
last_message: cached?.last_message,
last_message_at: cached?.last_message_at || cached?.updated_at || new Date().toISOString(),
unread_count: Number(cached?.unread_count || 0),
participants: Array.isArray(cached?.participants) ? cached.participants : [],
my_last_read_seq: Number(cached?.my_last_read_seq || 0),
other_last_read_seq: Number(cached?.other_last_read_seq || 0),
created_at: cached?.created_at || new Date().toISOString(),
updated_at: cached?.updated_at || new Date().toISOString(),
};
}
// ==================== 会话相关 ====================
/**
* 获取会话列表
* GET /api/v1/conversations/list
*/
async getConversations(
page = 1,
pageSize = 20,
forceRefresh = false
): Promise<ConversationListResponse> {
// 如果强制刷新,直接从服务器获取,不使用缓存
if (!forceRefresh) {
const cachedList = await getConversationListCache();
if (cachedList.length > 0) {
// 后台刷新数据,但不阻塞当前返回
this.refreshConversationsFromServer(page, pageSize).catch(error => {
console.error('后台刷新会话列表失败:', error);
});
return {
list: cachedList,
total: cachedList.length,
page: 1,
page_size: cachedList.length,
total_pages: 1,
};
}
}
// 强制刷新或没有缓存时,直接从服务器获取
try {
return await this.refreshConversationsFromServer(page, pageSize);
} catch (error) {
console.error('获取会话列表失败:', error);
return {
list: [],
total: 0,
page: 1,
page_size: pageSize,
total_pages: 0,
};
}
}
/**
* 创建私聊会话
* POST /api/v1/conversations/create
* @param userId 目标用户ID (UUID格式)
*/
async createConversation(userId: string): Promise<ConversationResponse> {
const response = await api.post<ConversationResponse>('/conversations/create', {
user_id: userId,
});
await saveConversationCache(response.data);
if (response.data.participants?.length) {
await saveUsersCache(response.data.participants);
}
return response.data;
}
/**
* 获取会话详情
* GET /api/v1/conversations/get?conversation_id=xxx
*/
async getConversationById(id: string): Promise<ConversationDetailResponse> {
const cached = await getConversationCache(id);
if (cached) {
this.refreshConversationDetailFromServer(id).catch(error => {
console.error('后台刷新会话详情失败:', error);
});
return this.normalizeCachedDetail(id, cached);
}
try {
return await this.refreshConversationDetailFromServer(id);
} catch (error) {
console.error('获取会话详情失败:', error);
throw error;
}
}
/**
* 仅自己删除会话
* DELETE /api/v1/conversations/:id/self
*/
async deleteConversationForSelf(conversationId: string): Promise<void> {
await api.delete(`/conversations/${encodeURIComponent(conversationId)}/self`);
}
/**
* 设置会话置顶
* POST /api/v1/conversations/set_pinned
*/
async setConversationPinned(conversationId: string, isPinned: boolean): Promise<void> {
await api.post('/conversations/set_pinned', {
conversation_id: conversationId,
is_pinned: isPinned,
});
}
// ==================== 消息相关 ====================
/**
* 获取消息列表(支持增量同步和历史消息加载)
* GET /api/v1/conversations/get_messages?conversation_id=xxx
* @param conversationId 会话ID
* @param afterSeq 获取此序号之后的消息(增量同步,获取新消息)
* @param beforeSeq 获取此序号之前的历史消息(下拉加载更多,获取旧消息)
* @param limit 返回消息数量限制
*/
async getMessages(
conversationId: string,
afterSeq?: number,
beforeSeq?: number,
limit?: number
): Promise<MessageListResponse> {
const params: Record<string, any> = {
conversation_id: conversationId,
};
if (afterSeq !== undefined) {
params.after_seq = afterSeq;
}
if (beforeSeq !== undefined) {
params.before_seq = beforeSeq;
}
if (limit !== undefined) {
params.limit = limit;
}
try {
const response = await api.get<any>(
'/conversations/get_messages',
params
);
// 处理增量同步响应after_seq 或 before_seq
if ((afterSeq !== undefined || beforeSeq !== undefined) && response.data.messages !== undefined) {
return {
messages: response.data.messages,
total: response.data.messages.length,
page: 1,
page_size: limit || 100,
};
}
// 处理分页响应(后端返回 list 字段)
const data = response.data;
return {
messages: data.list || [],
total: data.total || 0,
page: data.page || 1,
page_size: data.page_size || 20,
};
} catch (error) {
console.error('获取消息列表失败:', error);
return {
messages: [],
total: 0,
page: 1,
page_size: 20,
};
}
}
/**
* 发送消息(新格式)
* POST /api/v1/conversations/send_message
* Body: { detail_type, conversation_id, message }
*/
async sendMessage(
conversationId: string,
data: SendMessageRequest,
detailType: 'private' | 'group' = 'private'
): Promise<MessageResponse> {
// 直接使用传入的 segments
if (!data.segments || data.segments.length === 0) {
throw new Error('消息内容不能为空');
}
return this.sendMessageByAction(
detailType,
conversationId,
data.segments
);
}
/**
* 通过 HTTP POST 发送消息(新格式)
* 请求格式: POST /api/v1/conversations/send_message
* Body: { "detail_type": "private", "conversation_id": "xxx", "segments": [...] }
* @param detailType 消息类型: 'private' 私聊, 'group' 群聊
* @param conversationId 会话ID
* @param message 消息段数组 [{"type": "text", "data": {"text": "xxx"}}]
* @returns 消息响应
*/
async sendMessageByAction(
detailType: 'private' | 'group',
conversationId: string,
message: MessageSegment[]
): Promise<MessageResponse> {
try {
const response = await api.post<MessageResponse>(
'/conversations/send_message',
{
detail_type: detailType,
conversation_id: conversationId,
segments: message,
}
);
return response.data;
} catch (error) {
console.error('发送消息失败:', error);
throw error;
}
}
/**
* 发送文本消息(便捷方法)
*/
async sendTextMessage(
conversationId: string,
content: string,
detailType: 'private' | 'group' = 'private'
): Promise<MessageResponse> {
return this.sendTextMessageByAction(detailType, conversationId, content);
}
/**
* 通过 HTTP POST 发送文本消息
* @param detailType 消息类型: 'private' 私聊, 'group' 群聊
* @param conversationId 会话ID
* @param text 文本内容
* @returns 消息响应
*/
async sendTextMessageByAction(
detailType: 'private' | 'group',
conversationId: string,
text: string
): Promise<MessageResponse> {
const message: MessageSegment[] = [
{
type: 'text',
data: {
text: text,
},
},
];
return this.sendMessageByAction(detailType, conversationId, message);
}
/**
* 发送图片消息(便捷方法)
*/
async sendImageMessage(
conversationId: string,
mediaUrl: string,
content?: string,
detailType: 'private' | 'group' = 'private'
): Promise<MessageResponse> {
const message: MessageSegment[] = [
{
type: 'image',
data: {
url: mediaUrl,
},
},
];
// 如果有文本说明,添加一个文本段
if (content) {
message.unshift({
type: 'text',
data: { text: content },
});
}
return this.sendMessageByAction(detailType, conversationId, message);
}
/**
* 发送 segments 格式消息(新版统一格式)
* @param conversationId 会话ID
* @param segments 消息链数组
* @param detailType 消息类型
*/
async sendSegmentsMessage(
conversationId: string,
segments: MessageSegment[],
detailType: 'private' | 'group' = 'private'
): Promise<MessageResponse> {
return this.sendMessageByAction(detailType, conversationId, segments);
}
/**
* 发送带回复的 segments 消息
* @param conversationId 会话ID
* @param segments 消息链数组
* @param replyToId 被回复消息的ID
* @param detailType 消息类型
*/
async sendReplySegmentsMessage(
conversationId: string,
segments: MessageSegment[],
replyToId: string,
detailType: 'private' | 'group' = 'private'
): Promise<MessageResponse> {
// 添加回复段到消息开头
const replySegment: MessageSegment = {
type: 'reply',
data: {
id: replyToId,
},
};
return this.sendMessageByAction(detailType, conversationId, [replySegment, ...segments]);
}
/**
* 撤回/删除消息
* POST /api/v1/messages/delete_msg
* Body: { "message_id": "xxx" }
*/
async recallMessage(messageId: string): Promise<void> {
await api.post('/messages/delete_msg', {
message_id: messageId,
});
}
/**
* 删除消息(同撤回)
* POST /api/v1/messages/delete_msg
*/
async deleteMessage(messageId: string): Promise<void> {
await this.recallMessage(messageId);
}
// ==================== 已读相关 ====================
/**
* 标记已读
* POST /api/v1/conversations/mark_read
* @param conversationId 会话ID
* @param seq 已读到的消息序号
*/
async markAsRead(conversationId: string, seq: number): Promise<void> {
await api.post('/conversations/mark_read', {
conversation_id: conversationId,
last_read_seq: Number(seq),
});
// 立即清零本地缓存中的 unread_count避免回到列表时仍显示旧红点
updateConversationCacheUnreadCount(conversationId, 0).catch(error => {
console.error('更新本地会话未读数失败:', error);
});
}
/**
* 获取未读总数
* GET /api/v1/conversations/unread/count
*/
async getUnreadCount(): Promise<UnreadCountResponse> {
try {
const response = await api.get<UnreadCountResponse>(
'/conversations/unread/count'
);
return response.data;
} catch (error) {
console.error('获取未读总数失败:', error);
return { total_unread_count: 0 };
}
}
/**
* 获取单个会话未读数
* GET /api/v1/conversations/unread/count?conversation_id=xxx
*/
async getConversationUnreadCount(
conversationId: string
): Promise<ConversationUnreadCountResponse> {
try {
const response = await api.get<ConversationUnreadCountResponse>(
'/conversations/unread/count',
{ conversation_id: conversationId }
);
return response.data;
} catch (error) {
console.error('获取会话未读数失败:', error);
return {
conversation_id: conversationId,
unread_count: 0,
};
}
}
// ==================== 系统消息相关 ====================
/**
* 获取系统消息列表
* GET /api/v1/messages/system
* @param limit 返回数量限制
* @param beforeId 获取此ID之前的消息用于分页
*/
async getSystemMessages(
pageSize: number = 20,
page: number = 1
): Promise<SystemMessageListResponse> {
try {
const response = await api.get<{
list: any[];
total: number;
page: number;
page_size: number;
total_pages: number;
}>('/messages/system', {
page,
page_size: pageSize,
});
const data = response.data;
return {
messages: data.list || [],
total: data.total || 0,
has_more: data.page < data.total_pages,
};
} catch (error) {
console.error('获取系统消息列表失败:', error);
return {
messages: [],
total: 0,
has_more: false,
};
}
}
/**
* 获取系统消息未读数
* GET /api/v1/messages/system/unread-count
*/
async getSystemUnreadCount(): Promise<SystemUnreadCountResponse> {
try {
const response = await api.get<SystemUnreadCountResponse>(
'/messages/system/unread-count'
);
return response.data;
} catch (error) {
console.error('获取系统消息未读数失败:', error);
return { unread_count: 0 };
}
}
/**
* 标记单条系统消息已读
* PUT /api/v1/messages/system/:messageId/read
* @param messageId 消息ID
*/
async markSystemMessageRead(messageId: string): Promise<void> {
await api.put(`/messages/system/${encodeURIComponent(messageId)}/read`);
}
/**
* 标记所有系统消息已读
* PUT /api/v1/messages/system/read-all
*/
async markAllSystemMessagesRead(): Promise<void> {
await api.put('/messages/system/read-all');
}
// ==================== 兼容旧API的方法将逐步废弃 ====================
/**
* @deprecated 使用 getConversations 代替
*/
async getConversation(conversationId: string): Promise<ConversationDetailResponse | null> {
try {
return await this.getConversationById(conversationId);
} catch (error) {
console.error('获取会话详情失败:', error);
return null;
}
}
/**
* @deprecated 使用 createConversation(userId: string) 代替
*/
async createConversationByString(userId: string): Promise<ConversationResponse | null> {
try {
return await this.createConversation(userId);
} catch (error) {
console.error('创建会话失败:', error);
return null;
}
}
}
// 导出消息服务实例
export const messageService = new MessageService();
// 导出类型以方便使用
export type {
ConversationListResponse,
ConversationResponse,
ConversationDetailResponse,
MessageListResponse,
MessageResponse,
SendMessageRequest,
UnreadCountResponse,
ConversationUnreadCountResponse,
SystemMessageListResponse,
SystemUnreadCountResponse,
};

View File

@@ -0,0 +1,157 @@
/**
* 通知服务
* 处理系统通知、互动通知等功能
*/
import { api, PaginatedData } from './api';
import { Notification, NotificationBadge, NotificationType } from '../types';
// 通知列表响应
interface NotificationListResponse {
list: Notification[];
total: number;
page: number;
page_size: number;
total_pages: number;
}
// 通知角标响应
interface NotificationBadgeResponse {
total: number;
likes: number;
comments: number;
follows: number;
system: number;
}
// 通知响应
type NotificationResponse = Notification;
// 通知服务类
class NotificationService {
// 获取通知列表
async getNotifications(
page = 1,
pageSize = 20,
type?: NotificationType
): Promise<PaginatedData<Notification>> {
try {
const params: Record<string, any> = {
page,
page_size: pageSize,
};
if (type) {
params.type = type;
}
const response = await api.get<NotificationListResponse>('/notifications', params);
return response.data;
} catch (error) {
console.error('获取通知列表失败:', error);
return {
list: [],
total: 0,
page: 1,
page_size: pageSize,
total_pages: 0,
};
}
}
// 获取未读通知数
async getUnreadCount(): Promise<number> {
try {
const response = await api.get<{ count: number }>('/notifications/unread-count');
return response.data.count || 0;
} catch (error) {
console.error('获取未读通知数失败:', error);
return 0;
}
}
// 获取通知角标
async getNotificationBadge(): Promise<NotificationBadge> {
try {
const response = await api.get<NotificationBadgeResponse>('/notifications/badge');
return {
total: response.data.total || 0,
likes: response.data.likes || 0,
comments: response.data.comments || 0,
follows: response.data.follows || 0,
system: response.data.system || 0,
};
} catch (error) {
console.error('获取通知角标失败:', error);
return {
total: 0,
likes: 0,
comments: 0,
follows: 0,
system: 0,
};
}
}
// 标记单个通知为已读
async markAsRead(notificationId: string): Promise<boolean> {
try {
const response = await api.post(`/notifications/${notificationId}/read`);
return response.code === 0;
} catch (error) {
console.error('标记通知为已读失败:', error);
return false;
}
}
// 标记所有通知为已读
async markAllAsRead(): Promise<boolean> {
try {
const response = await api.post('/notifications/read-all');
return response.code === 0;
} catch (error) {
console.error('标记所有通知为已读失败:', error);
return false;
}
}
// 删除通知
async deleteNotification(notificationId: string): Promise<boolean> {
try {
const response = await api.delete(`/notifications/${notificationId}`);
return response.code === 0;
} catch (error) {
console.error('删除通知失败:', error);
return false;
}
}
// 清空所有通知
async clearAll(): Promise<boolean> {
try {
const response = await api.delete('/notifications');
return response.code === 0;
} catch (error) {
console.error('清空通知失败:', error);
return false;
}
}
// 获取互动通知(点赞、评论、关注)
async getInteractionNotifications(page = 1, pageSize = 20): Promise<PaginatedData<Notification>> {
return this.getNotifications(page, pageSize, 'like_post');
}
// 获取系统通知
async getSystemNotifications(page = 1, pageSize = 20): Promise<PaginatedData<Notification>> {
return this.getNotifications(page, pageSize, 'system');
}
// 获取@我的通知
async getMentionNotifications(page = 1, pageSize = 20): Promise<PaginatedData<Notification>> {
return this.getNotifications(page, pageSize, 'mention');
}
}
// 导出通知服务实例
export const notificationService = new NotificationService();

299
src/services/postService.ts Normal file
View File

@@ -0,0 +1,299 @@
/**
* 帖子服务
* 处理帖子的增删改查、点赞、收藏等功能
*/
import { api, PaginatedData } from './api';
import { Post, CreatePostInput } from '../types';
// 帖子列表响应
interface PostListResponse {
list: Post[];
total: number;
page: number;
page_size: number;
total_pages: number;
}
// 帖子响应 - API直接返回Post对象
type PostResponse = Post;
// 创建帖子请求
interface CreatePostRequest {
title: string;
content: string;
images?: string[];
}
// 更新帖子请求
interface UpdatePostRequest {
title?: string;
content?: string;
images?: string[];
}
// 帖子服务类
class PostService {
// 获取帖子列表
async getPosts(
page = 1,
pageSize = 20,
tab?: string,
communityId?: string
): Promise<PaginatedData<Post>> {
const params: Record<string, any> = {
page,
page_size: pageSize,
};
if (tab) {
params.tab = tab;
}
if (communityId) {
params.community_id = communityId;
}
const response = await api.get<PostListResponse>('/posts', params);
return response.data;
}
// 获取推荐帖子
async getRecommendedPosts(page = 1, pageSize = 20): Promise<PaginatedData<Post>> {
return this.getPosts(page, pageSize, 'recommend');
}
// 获取关注帖子
async getFollowingPosts(page = 1, pageSize = 20): Promise<PaginatedData<Post>> {
return this.getPosts(page, pageSize, 'follow');
}
// 获取热门帖子
async getHotPosts(page = 1, pageSize = 20): Promise<PaginatedData<Post>> {
return this.getPosts(page, pageSize, 'hot');
}
// 获取最新帖子
async getLatestPosts(page = 1, pageSize = 20): Promise<PaginatedData<Post>> {
return this.getPosts(page, pageSize, 'latest');
}
// 获取帖子详情(不增加浏览量)
async getPost(postId: string): Promise<Post | null> {
try {
const response = await api.get<PostResponse>(`/posts/${postId}`);
return response.data;
} catch (error) {
console.error('获取帖子详情失败:', error);
return null;
}
}
// 记录帖子浏览(增加浏览量)
async recordView(postId: string): Promise<boolean> {
try {
const response = await api.post(`/posts/${postId}/view`);
return response.code === 0;
} catch (error) {
console.error('记录浏览失败:', error);
return false;
}
}
// 创建帖子
async createPost(data: CreatePostRequest): Promise<Post | null> {
const response = await api.post<PostResponse>('/posts', data);
return response.data;
}
// 更新帖子
async updatePost(postId: string, data: UpdatePostRequest): Promise<Post | null> {
try {
const response = await api.put<PostResponse>(`/posts/${postId}`, data);
return response.data;
} catch (error) {
console.error('更新帖子失败:', error);
return null;
}
}
// 删除帖子
async deletePost(postId: string): Promise<boolean> {
try {
const response = await api.delete(`/posts/${postId}`);
return response.code === 0;
} catch (error) {
console.error('删除帖子失败:', error);
return false;
}
}
// 点赞帖子
async likePost(postId: string): Promise<Post | null> {
try {
console.log('[postService] likePost called, postId:', postId);
const response = await api.post<Post>(`/posts/${postId}/like`);
console.log('[postService] likePost response:', response);
if (response.code === 0) {
console.log('[postService] likePost success, data:', response.data);
return response.data;
}
console.warn('[postService] likePost failed, code:', response.code);
return null;
} catch (error) {
console.error('[postService] likePost error:', error);
return null;
}
}
// 取消点赞帖子
async unlikePost(postId: string): Promise<Post | null> {
try {
console.log('[postService] unlikePost called, postId:', postId);
const response = await api.delete<Post>(`/posts/${postId}/like`);
console.log('[postService] unlikePost response:', response);
if (response.code === 0) {
console.log('[postService] unlikePost success, data:', response.data);
return response.data;
}
console.warn('[postService] unlikePost failed, code:', response.code);
return null;
} catch (error) {
console.error('[postService] unlikePost error:', error);
return null;
}
}
// 收藏帖子
async favoritePost(postId: string): Promise<Post | null> {
try {
console.log('[postService] favoritePost called, postId:', postId);
const response = await api.post<Post>(`/posts/${postId}/favorite`);
console.log('[postService] favoritePost response:', response);
if (response.code === 0) {
console.log('[postService] favoritePost success, data:', response.data);
return response.data;
}
console.warn('[postService] favoritePost failed, code:', response.code);
return null;
} catch (error) {
console.error('[postService] favoritePost error:', error);
return null;
}
}
// 取消收藏帖子
async unfavoritePost(postId: string): Promise<Post | null> {
try {
console.log('[postService] unfavoritePost called, postId:', postId);
const response = await api.delete<Post>(`/posts/${postId}/favorite`);
console.log('[postService] unfavoritePost response:', response);
if (response.code === 0) {
console.log('[postService] unfavoritePost success, data:', response.data);
return response.data;
}
console.warn('[postService] unfavoritePost failed, code:', response.code);
return null;
} catch (error) {
console.error('[postService] unfavoritePost error:', error);
return null;
}
}
// 获取用户帖子列表
async getUserPosts(
userId: string,
page = 1,
pageSize = 20
): Promise<PaginatedData<Post>> {
try {
const response = await api.get<PostListResponse>(`/users/${userId}/posts`, {
page,
page_size: pageSize,
});
return response.data;
} catch (error) {
console.error('获取用户帖子列表失败:', error);
return {
list: [],
total: 0,
page: 1,
page_size: pageSize,
total_pages: 0,
};
}
}
// 获取用户收藏列表
async getUserFavorites(
userId: string,
page = 1,
pageSize = 20
): Promise<PaginatedData<Post>> {
try {
const response = await api.get<PostListResponse>(`/users/${userId}/favorites`, {
page,
page_size: pageSize,
});
return response.data;
} catch (error) {
console.error('获取用户收藏列表失败:', error);
return {
list: [],
total: 0,
page: 1,
page_size: pageSize,
total_pages: 0,
};
}
}
// 搜索帖子
async searchPosts(
keyword: string,
page = 1,
pageSize = 20
): Promise<PaginatedData<Post>> {
try {
const response = await api.get<PostListResponse>('/posts/search', {
keyword,
page,
page_size: pageSize,
});
return response.data;
} catch (error) {
console.error('搜索帖子失败:', error);
return {
list: [],
total: 0,
page: 1,
page_size: pageSize,
total_pages: 0,
};
}
}
// 分享帖子
async sharePost(postId: string): Promise<boolean> {
try {
const response = await api.post(`/posts/${postId}/share`);
return response.code === 0;
} catch (error) {
console.error('分享帖子失败:', error);
return false;
}
}
// 举报帖子
async reportPost(postId: string, reason: string): Promise<boolean> {
try {
const response = await api.post(`/posts/${postId}/report`, { reason });
return response.code === 0;
} catch (error) {
console.error('举报帖子失败:', error);
return false;
}
}
}
// 导出帖子服务实例
export const postService = new PostService();

View File

@@ -0,0 +1,25 @@
export type PromptType = 'info' | 'success' | 'warning' | 'error';
export interface PromptPayload {
title?: string;
message: string;
type?: PromptType;
duration?: number;
}
type PromptListener = (payload: PromptPayload) => void;
let promptListener: PromptListener | null = null;
export const bindPromptListener = (listener: PromptListener) => {
promptListener = listener;
return () => {
if (promptListener === listener) {
promptListener = null;
}
};
};
export const showPrompt = (payload: PromptPayload) => {
promptListener?.(payload);
};

111
src/services/pushService.ts Normal file
View File

@@ -0,0 +1,111 @@
/**
* 推送服务 - 管理设备Token和推送记录
*/
import { api } from './api';
import {
DeviceTokenResponse,
RegisterDeviceRequest,
PushRecordResponse
} from '../types/dto';
// 推送服务类
class PushService {
/**
* 注册设备
* POST /api/v1/push/devices
* @param data 设备注册信息
*/
async registerDevice(data: RegisterDeviceRequest): Promise<DeviceTokenResponse> {
const response = await api.post<DeviceTokenResponse>('/push/devices', data);
return response.data;
}
/**
* 获取当前用户的所有设备列表
* GET /api/v1/push/devices
*/
async getDevices(): Promise<DeviceTokenResponse[]> {
try {
const response = await api.get<DeviceTokenResponse[]>('/push/devices');
return response.data;
} catch (error) {
console.error('获取设备列表失败:', error);
return [];
}
}
/**
* 注销设备删除设备Token
* DELETE /api/v1/push/devices/:deviceId
* @param deviceId 设备ID
*/
async unregisterDevice(deviceId: string): Promise<void> {
await api.delete(`/push/devices/${deviceId}`);
}
/**
* 更新设备推送Token
* PUT /api/v1/push/devices/:deviceId/token
* @param deviceId 设备ID
* @param pushToken 新的推送Token
*/
async updateDeviceToken(deviceId: string, pushToken: string): Promise<void> {
await api.put(`/push/devices/${deviceId}/token`, { push_token: pushToken });
}
/**
* 获取推送记录
* GET /api/v1/push/records
* @param limit 返回数量限制
* @param offset 偏移量
*/
async getPushRecords(limit?: number, offset?: number): Promise<PushRecordResponse[]> {
const params: Record<string, any> = {};
if (limit !== undefined) {
params.limit = limit;
}
if (offset !== undefined) {
params.offset = offset;
}
try {
const response = await api.get<PushRecordResponse[]>('/push/records', params);
return response.data;
} catch (error) {
console.error('获取推送记录失败:', error);
return [];
}
}
/**
* 设置设备活跃状态
* PUT /api/v1/push/devices/:deviceId/active
* @param deviceId 设备ID
* @param isActive 是否活跃
*/
async setDeviceActive(deviceId: string, isActive: boolean): Promise<void> {
await api.put(`/push/devices/${deviceId}/active`, { is_active: isActive });
}
}
// 导出推送服务实例
export const pushService = new PushService();
// 导出便捷方法(与任务要求保持一致)
export const registerDevice = (data: RegisterDeviceRequest): Promise<DeviceTokenResponse> =>
pushService.registerDevice(data);
export const getDevices = (): Promise<DeviceTokenResponse[]> =>
pushService.getDevices();
export const unregisterDevice = (deviceId: string): Promise<void> =>
pushService.unregisterDevice(deviceId);
export const updateDeviceToken = (deviceId: string, pushToken: string): Promise<void> =>
pushService.updateDeviceToken(deviceId, pushToken);
export const getPushRecords = (limit?: number, offset?: number): Promise<PushRecordResponse[]> =>
pushService.getPushRecords(limit, offset);
export default pushService;

View File

@@ -0,0 +1,145 @@
/**
* 自定义表情服务
* 管理用户自定义添加的表情包(云端存储)
*/
import { api } from './api';
import { uploadService } from './uploadService';
// 自定义表情类型
export interface CustomSticker {
id: string;
user_id: string;
url: string;
width: number;
height: number;
sort_order: number;
created_at: string;
updated_at: string;
}
// API 响应类型
interface GetStickersResponse {
stickers: CustomSticker[];
}
interface AddStickerResponse {
sticker: CustomSticker;
}
interface CheckStickerResponse {
exists: boolean;
}
/**
* 获取所有自定义表情
*/
export const getCustomStickers = async (): Promise<CustomSticker[]> => {
try {
const response = await api.get<GetStickersResponse>('/stickers');
return response.data.stickers || [];
} catch (error) {
console.error('获取自定义表情失败:', error);
return [];
}
};
/**
* 添加自定义表情从URL
*/
export const addStickerFromUrl = async (
imageUrl: string,
width?: number,
height?: number
): Promise<CustomSticker | null> => {
try {
let finalUrl = imageUrl;
const lowerUrl = imageUrl.toLowerCase();
// 本地图片 URI 不能直接落库,先上传换成可访问的远程 URL
if (lowerUrl.startsWith('file://') || lowerUrl.startsWith('content://')) {
const uploadResult = await uploadService.uploadImage({ uri: imageUrl });
if (!uploadResult?.url) {
throw new Error('upload sticker image failed');
}
finalUrl = uploadResult.url;
}
const response = await api.post<AddStickerResponse>('/stickers', {
url: finalUrl,
width: width || 0,
height: height || 0,
});
return response.data.sticker;
} catch (error: any) {
if (error.response?.status === 409) {
console.log('表情已存在');
return null;
}
console.error('添加自定义表情失败:', error);
throw error;
}
};
/**
* 删除自定义表情
*/
export const deleteSticker = async (stickerId: string): Promise<boolean> => {
try {
await api.delete('/stickers', {
data: { sticker_id: stickerId },
});
return true;
} catch (error) {
console.error('删除自定义表情失败:', error);
return false;
}
};
/**
* 检查图片是否已经是自定义表情
*/
export const isStickerExists = async (imageUrl: string): Promise<boolean> => {
try {
const response = await api.get<CheckStickerResponse>('/stickers/check', { url: imageUrl });
return response.data.exists;
} catch (error) {
console.error('检查表情是否存在失败:', error);
return false;
}
};
/**
* 重新排序表情
*/
export const reorderStickers = async (orders: Record<string, number>): Promise<boolean> => {
try {
await api.post('/stickers/reorder', { orders });
return true;
} catch (error) {
console.error('重新排序表情失败:', error);
return false;
}
};
/**
* 批量删除表情
*/
export const batchDeleteStickers = async (stickerIds: string[]): Promise<{ success: number; failed: number }> => {
let success = 0;
let failed = 0;
for (const stickerId of stickerIds) {
try {
await api.delete('/stickers', {
data: { sticker_id: stickerId },
});
success++;
} catch (error) {
console.error(`删除表情 ${stickerId} 失败:`, error);
failed++;
}
}
return { success, failed };
};

View File

@@ -0,0 +1,230 @@
/**
* 系统通知服务
* 使用 expo-notifications 处理原生系统通知
* 支持本地通知,类似 QQ 在通知栏显示消息
*/
import * as Notifications from 'expo-notifications';
import { Platform, AppState, AppStateStatus } from 'react-native';
import type { WSChatMessage, WSNotificationMessage, WSAnnouncementMessage } from './websocketService';
import { extractTextFromSegments } from '../types/dto';
// 通知渠道配置
const CHANNEL_ID = 'default';
const CHANNEL_NAME = '默认通知';
const CHANNEL_DESCRIPTION = '接收系统消息、互动通知等';
// 通知类型
export type AppNotificationType =
| 'like_post'
| 'like_comment'
| 'like_reply'
| 'favorite_post'
| 'comment'
| 'reply'
| 'follow'
| 'mention'
| 'system'
| 'announcement'
| 'announce'
| 'chat';
// 获取通知标题
export function getNotificationTitle(type: AppNotificationType, operatorName?: string): string {
const titles: Record<AppNotificationType, string> = {
like_post: '有人赞了你的帖子',
like_comment: '有人赞了你的评论',
like_reply: '有人赞了你的回复',
favorite_post: '有人收藏了你的帖子',
comment: '新评论',
reply: '新回复',
follow: '新粉丝',
mention: '@提及',
system: '系统通知',
announcement: '公告',
announce: '公告',
chat: '新消息',
};
return titles[type] || '新通知';
}
class SystemNotificationService {
private isInitialized: boolean = false;
private appStateSubscription: any = null;
private currentAppState: AppStateStatus = 'active';
async initialize(): Promise<boolean> {
if (this.isInitialized) {
return true;
}
try {
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
console.log('[SystemNotification] 通知权限未授权');
return false;
}
// 设置 notification handler确保前台也能显示通知、播放声音和震动
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
shouldShowBanner: true,
shouldShowList: true,
}),
});
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync(CHANNEL_ID, {
name: CHANNEL_NAME,
importance: Notifications.AndroidImportance.HIGH, // HIGH 优先级足以触发震动
vibrationPattern: [0, 300, 200, 300], // 等待0ms震动300ms停止200ms震动300ms
lightColor: '#FF6B35',
enableVibrate: true, // 启用震动
enableLights: true, // 启用呼吸灯
});
}
this.currentAppState = AppState.currentState;
this.appStateSubscription = AppState.addEventListener('change', (nextAppState) => {
this.currentAppState = nextAppState;
});
this.isInitialized = true;
console.log('[SystemNotification] 通知服务初始化成功');
return true;
} catch (error) {
console.error('[SystemNotification] 初始化失败:', error);
return false;
}
}
async showNotification(options: {
title: string;
body: string;
data?: Record<string, string | number | object>;
type: AppNotificationType;
}): Promise<string | null> {
if (!this.isInitialized) {
await this.initialize();
}
try {
// 使用 scheduleNotificationAsync 立即显示通知
// 配合 setNotificationHandler 确保前台也能显示
// 构建通知内容
const content: Notifications.NotificationContentInput = {
title: options.title,
body: options.body,
data: options.data as Record<string, string | number | object> | undefined,
// 显式设置震动(仅 Android 生效)
...(Platform.OS === 'android' ? {
vibrationPattern: [0, 300, 200, 300],
} : {}),
};
const notificationId = await Notifications.scheduleNotificationAsync({
content,
trigger: null, // null 表示立即显示
});
console.log('[SystemNotification] 通知已显示:', notificationId);
return notificationId;
} catch (error) {
console.error('[SystemNotification] 显示通知失败:', error);
return null;
}
}
async showChatNotification(message: WSChatMessage): Promise<string | null> {
const body = extractTextFromSegments(message.segments);
return this.showNotification({
title: '新消息',
body,
data: {
type: 'chat',
conversationId: message.conversation_id,
messageId: String(message.id),
senderId: message.sender_id,
},
type: 'chat',
});
}
async showWSNotification(message: WSNotificationMessage | WSAnnouncementMessage): Promise<string | null> {
const type = message.system_type as AppNotificationType;
const title = getNotificationTitle(type);
const body = message.content;
return this.showNotification({
title,
body,
data: {
type: message.type,
id: String(message.id),
senderId: message.sender_id,
receiverId: message.receiver_id,
systemType: message.system_type,
extraData: JSON.stringify(message.extra_data || {}),
},
type,
});
}
async handleWSMessage(message: WSChatMessage | WSNotificationMessage | WSAnnouncementMessage): Promise<void> {
console.log('[SystemNotification] handleWSMessage 被调用, 当前AppState:', this.currentAppState);
// 仅在后台时显示通知,前台时不显示(用户正在使用应用,可以直接看到消息)
if (this.currentAppState !== 'active') {
// 判断是否是聊天消息(通过 segments 字段)
if ('segments' in message) {
const chatMsg = message as WSChatMessage;
const body = extractTextFromSegments(chatMsg.segments);
console.log('[SystemNotification] 后台模式 - 显示聊天通知:', chatMsg.id, body);
await this.showChatNotification(chatMsg);
} else {
const notifMsg = message as WSNotificationMessage | WSAnnouncementMessage;
console.log('[SystemNotification] 后台模式 - 显示系统通知:', notifMsg.id, notifMsg.content);
await this.showWSNotification(notifMsg);
}
} else {
console.log('[SystemNotification] 前台模式 - 不显示通知');
}
}
async clearAllNotifications(): Promise<void> {
await Notifications.dismissAllNotificationsAsync();
}
async clearBadge(): Promise<void> {
await Notifications.setBadgeCountAsync(0);
}
async setBadgeCount(count: number): Promise<void> {
await Notifications.setBadgeCountAsync(count);
}
cleanup(): void {
if (this.appStateSubscription) {
this.appStateSubscription.remove();
this.appStateSubscription = null;
}
this.isInitialized = false;
}
getAppState(): AppStateStatus {
return this.currentAppState;
}
}
export const systemNotificationService = new SystemNotificationService();

View File

@@ -0,0 +1,285 @@
/**
* 文件上传服务
* 处理图片、视频等文件上传功能
*/
import { api } from './api';
/**
* 根据 URI 扩展名和 mimeType 推断正确的文件信息
* 优先使用 providedType其次从 URI 推断,最后默认 JPEG
* 这对 GIF 至关重要Content-Type 错误会导致后端存为 .jpg丢失动画
*/
function resolveFileInfo(uri: string, providedType?: string): { name: string; type: string } {
const ts = Date.now();
// 先从 providedType 判断
if (providedType === 'image/gif') return { name: `img_${ts}.gif`, type: 'image/gif' };
if (providedType === 'image/png') return { name: `img_${ts}.png`, type: 'image/png' };
if (providedType === 'image/webp') return { name: `img_${ts}.webp`, type: 'image/webp' };
if (providedType === 'video/mp4') return { name: `video_${ts}.mp4`, type: 'video/mp4' };
if (providedType === 'video/quicktime') return { name: `video_${ts}.mov`, type: 'video/quicktime' };
// 再从 URI 路径推断expo-image-picker 返回的 URI 通常包含原始扩展名)
const uriLower = uri.toLowerCase();
if (uriLower.includes('.gif')) return { name: `img_${ts}.gif`, type: 'image/gif' };
if (uriLower.includes('.png')) return { name: `img_${ts}.png`, type: 'image/png' };
if (uriLower.includes('.webp')) return { name: `img_${ts}.webp`, type: 'image/webp' };
if (uriLower.includes('.mp4')) return { name: `video_${ts}.mp4`, type: 'video/mp4' };
if (uriLower.includes('.mov')) return { name: `video_${ts}.mov`, type: 'video/quicktime' };
// 默认 JPEG
return { name: `img_${ts}.jpg`, type: providedType || 'image/jpeg' };
}
// 上传响应
export interface UploadResponse {
url: string;
width?: number;
height?: number;
size?: number;
type?: string;
}
// 上传服务类
class UploadService {
// 上传图片
async uploadImage(file: {
uri: string;
name?: string;
type?: string;
}): Promise<UploadResponse | null> {
try {
const { name, type } = resolveFileInfo(file.uri, file.type);
const response = await api.upload<UploadResponse>('/uploads/images', {
uri: file.uri,
name: file.name || name,
type,
});
return response.data;
} catch (error) {
console.error('上传图片失败:', error);
return null;
}
}
// 上传多张图片
async uploadImages(files: {
uri: string;
name?: string;
type?: string;
}[]): Promise<UploadResponse[]> {
const results: UploadResponse[] = [];
for (const file of files) {
const result = await this.uploadImage(file);
if (result) {
results.push(result);
}
}
return results;
}
// 上传头像
async uploadAvatar(file: {
uri: string;
name?: string;
type?: string;
}): Promise<UploadResponse | null> {
try {
const { name, type } = resolveFileInfo(file.uri, file.type);
const response = await api.upload<UploadResponse>('/users/me/avatar', {
uri: file.uri,
name: file.name || name,
type,
});
return response.data;
} catch (error) {
console.error('上传头像失败:', error);
return null;
}
}
// 上传视频
async uploadVideo(file: {
uri: string;
name?: string;
type?: string;
}): Promise<UploadResponse | null> {
try {
const fileName = file.name || `video_${Date.now()}.mp4`;
const fileType = file.type || 'video/mp4';
const response = await api.upload<UploadResponse>('/upload/video', {
uri: file.uri,
name: fileName,
type: fileType,
});
return response.data;
} catch (error) {
console.error('上传视频失败:', error);
return null;
}
}
// 上传文件(通用)
async uploadFile(
file: {
uri: string;
name?: string;
type?: string;
},
folder: string = 'general'
): Promise<UploadResponse | null> {
try {
const response = await api.upload<UploadResponse>('/upload/file', {
uri: file.uri,
name: file.name || `file_${Date.now()}`,
type: file.type || 'application/octet-stream',
}, { folder });
return response.data;
} catch (error) {
console.error('上传文件失败:', error);
return null;
}
}
// 上传帖子图片
async uploadPostImages(files: {
uri: string;
name?: string;
type?: string;
}[]): Promise<string[]> {
const urls: string[] = [];
for (const file of files) {
const result = await this.uploadImage(file);
if (result) {
urls.push(result.url);
}
}
return urls;
}
// 上传聊天图片(复用上传图片接口)
async uploadChatImage(file: {
uri: string;
name?: string;
type?: string;
}): Promise<UploadResponse | null> {
// 使用通用的图片上传接口
return this.uploadImage(file);
}
// 上传社区图标
async uploadCommunityIcon(file: {
uri: string;
name?: string;
type?: string;
}): Promise<UploadResponse | null> {
try {
const fileName = file.name || `community_${Date.now()}.jpg`;
const fileType = file.type || 'image/jpeg';
const response = await api.upload<UploadResponse>('/upload/community-icon', {
uri: file.uri,
name: fileName,
type: fileType,
});
return response.data;
} catch (error) {
console.error('上传社区图标失败:', error);
return null;
}
}
// 上传社区封面图
async uploadCommunityCover(file: {
uri: string;
name?: string;
type?: string;
}): Promise<UploadResponse | null> {
try {
const fileName = file.name || `community_cover_${Date.now()}.jpg`;
const fileType = file.type || 'image/jpeg';
const response = await api.upload<UploadResponse>('/upload/community-cover', {
uri: file.uri,
name: fileName,
type: fileType,
});
return response.data;
} catch (error) {
console.error('上传社区封面图失败:', error);
return null;
}
}
// 删除文件
async deleteFile(url: string): Promise<boolean> {
try {
const response = await api.post('/upload/delete', { url });
return response.code === 0;
} catch (error) {
console.error('删除文件失败:', error);
return false;
}
}
// 上传群头像
async uploadGroupAvatar(file: {
uri: string;
name?: string;
type?: string;
}): Promise<UploadResponse | null> {
try {
const { name, type } = resolveFileInfo(file.uri, file.type);
const response = await api.upload<UploadResponse>('/uploads/images', {
uri: file.uri,
name: file.name || name,
type,
});
return response.data;
} catch (error) {
console.error('上传群头像失败:', error);
return null;
}
}
// 上传头图(个人主页封面)
async uploadCover(file: {
uri: string;
name?: string;
type?: string;
}): Promise<UploadResponse | null> {
try {
const { name, type } = resolveFileInfo(file.uri, file.type);
const response = await api.upload<UploadResponse>('/users/me/cover', {
uri: file.uri,
name: file.name || name,
type,
});
return response.data;
} catch (error) {
console.error('上传头图失败:', error);
return null;
}
}
}
// 导出上传服务实例
export const uploadService = new UploadService();

114
src/services/voteService.ts Normal file
View File

@@ -0,0 +1,114 @@
/**
* 投票服务
* 处理投票相关的API调用
*/
import { api } from './api';
import { Post, VoteResultDTO, CreateVotePostRequest } from '../types';
// 投票响应
interface VoteResponse {
success: boolean;
message?: string;
}
// 更新投票选项响应
interface UpdateVoteOptionResponse {
success: boolean;
message?: string;
}
/**
* 投票服务类
*/
class VoteService {
/**
* 创建投票帖子
* @param data 创建投票帖子请求数据
* @returns 创建的帖子或null
*/
async createVotePost(data: CreateVotePostRequest): Promise<Post | null> {
// 验证投票选项数量
if (!data.vote_options || data.vote_options.length < 2) {
console.error('[VoteService] 投票选项至少需要2个');
return null;
}
if (data.vote_options.length > 10) {
console.error('[VoteService] 投票选项最多10个');
return null;
}
const response = await api.post<Post>('/posts/vote', data);
return response.data;
}
/**
* 获取投票结果
* @param postId 帖子ID
* @returns 投票结果或null
*/
async getVoteResult(postId: string): Promise<VoteResultDTO | null> {
try {
const response = await api.get<VoteResultDTO>(`/posts/${postId}/vote`);
return response.data;
} catch (error) {
console.error('[VoteService] 获取投票结果失败:', error);
return null;
}
}
/**
* 投票
* @param postId 帖子ID
* @param optionId 选项ID
* @returns 是否成功
*/
async vote(postId: string, optionId: string): Promise<boolean> {
try {
const response = await api.post<VoteResponse>(`/posts/${postId}/vote`, {
option_id: optionId,
});
return response.data.success;
} catch (error) {
console.error('[VoteService] 投票失败:', error);
return false;
}
}
/**
* 取消投票
* @param postId 帖子ID
* @returns 是否成功
*/
async unvote(postId: string): Promise<boolean> {
try {
const response = await api.delete<VoteResponse>(`/posts/${postId}/vote`);
return response.data.success;
} catch (error) {
console.error('[VoteService] 取消投票失败:', error);
return false;
}
}
/**
* 更新投票选项(作者权限)
* @param optionId 选项ID
* @param content 新内容
* @param postId 帖子ID
* @returns 是否成功
*/
async updateVoteOption(optionId: string, content: string, postId: string): Promise<boolean> {
try {
const response = await api.put<UpdateVoteOptionResponse>(
`/posts/${postId}/vote/options/${optionId}`,
{ content }
);
return response.data.success;
} catch (error) {
console.error('[VoteService] 更新投票选项失败:', error);
return false;
}
}
}
export const voteService = new VoteService();

File diff suppressed because it is too large Load Diff