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:
55
src/services/alertOverride.ts
Normal file
55
src/services/alertOverride.ts
Normal 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
316
src/services/api.ts
Normal 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
436
src/services/authService.ts
Normal 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 + Token,DB 操作交给 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 + Token,DB 操作交给 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 + Token,DB/缓存清理交给 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();
|
||||
319
src/services/backgroundService.ts
Normal file
319
src/services/backgroundService.ts
Normal 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;
|
||||
246
src/services/commentService.ts
Normal file
246
src/services/commentService.ts
Normal 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
935
src/services/database.ts
Normal 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)]
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
55
src/services/dialogService.ts
Normal file
55
src/services/dialogService.ts
Normal 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 },
|
||||
});
|
||||
};
|
||||
391
src/services/groupService.ts
Normal file
391
src/services/groupService.ts
Normal 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
106
src/services/index.ts
Normal 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';
|
||||
597
src/services/messageService.ts
Normal file
597
src/services/messageService.ts
Normal 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,
|
||||
};
|
||||
157
src/services/notificationService.ts
Normal file
157
src/services/notificationService.ts
Normal 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
299
src/services/postService.ts
Normal 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();
|
||||
25
src/services/promptService.ts
Normal file
25
src/services/promptService.ts
Normal 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
111
src/services/pushService.ts
Normal 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;
|
||||
145
src/services/stickerService.ts
Normal file
145
src/services/stickerService.ts
Normal 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 };
|
||||
};
|
||||
230
src/services/systemNotificationService.ts
Normal file
230
src/services/systemNotificationService.ts
Normal 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();
|
||||
285
src/services/uploadService.ts
Normal file
285
src/services/uploadService.ts
Normal 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
114
src/services/voteService.ts
Normal 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();
|
||||
1140
src/services/websocketService.ts
Normal file
1140
src/services/websocketService.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user