feat: 优化架构
Some checks failed
Frontend CI / build-and-push-web (push) Successful in 2m39s
Frontend CI / ota-android (push) Successful in 10m22s
Frontend CI / build-android-apk (push) Has been cancelled

This commit is contained in:
lafay
2026-04-13 01:30:37 +08:00
parent 2adc9360a5
commit fe6a03da5d
93 changed files with 386 additions and 211 deletions

View File

@@ -0,0 +1,29 @@
import { api } from '../core/api';
export interface ChannelItem {
id: string;
name: string;
slug: string;
description?: string;
sort_order: number;
is_active: boolean;
}
interface ChannelListResponse {
list: ChannelItem[];
}
class ChannelService {
async list(): Promise<ChannelItem[]> {
try {
const res = await api.get<ChannelListResponse>('/channels');
return res.data?.list || [];
} catch (error) {
console.error('[channelService] 获取频道失败:', error);
return [];
}
}
}
export const channelService = new ChannelService();

View File

@@ -0,0 +1,309 @@
/**
* 评论服务
* 处理评论的增删改查、点赞等功能
*/
import { api, PaginatedData } from '../core/api';
import { Comment, CreateCommentInput } from '@/types';
import { CursorPaginationRequest, CursorPaginationResponse, CommentDTO } from '@/types/dto';
// 评论列表响应
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;
}
}
// ==================== 游标分页方法 ====================
/**
* 获取帖子评论列表(游标分页)
* GET /api/v1/comments/post/:id/cursor
* @param postId 帖子ID
* @param params 游标分页请求参数
*/
async getPostCommentsCursor(
postId: string,
params: CursorPaginationRequest = {}
): Promise<CursorPaginationResponse<Comment>> {
try {
const response = await api.get<any>(`/comments/post/${postId}/cursor`, params);
const raw = response.data;
return {
list: Array.isArray(raw?.list) ? raw.list : [],
next_cursor: raw?.next_cursor ?? null,
prev_cursor: raw?.prev_cursor ?? null,
has_more: raw?.has_more ?? false,
};
} catch (error) {
console.error('获取帖子评论列表失败:', error);
return {
list: [],
next_cursor: null,
prev_cursor: null,
has_more: false,
};
}
}
/**
* 获取评论回复列表(游标分页)
* GET /api/v1/comments/:id/replies/cursor
* @param commentId 评论ID
* @param params 游标分页请求参数
*/
async getCommentRepliesCursor(
commentId: string,
params: CursorPaginationRequest = {}
): Promise<CursorPaginationResponse<Comment>> {
try {
const response = await api.get<any>(`/comments/${commentId}/replies/cursor`, params);
const raw = response.data;
return {
list: Array.isArray(raw?.list) ? raw.list : [],
next_cursor: raw?.next_cursor ?? null,
prev_cursor: raw?.prev_cursor ?? null,
has_more: raw?.has_more ?? false,
};
} catch (error) {
console.error('获取评论回复列表失败:', error);
return {
list: [],
next_cursor: null,
prev_cursor: null,
has_more: false,
};
}
}
}
// 导出评论服务实例
export const commentService = new CommentService();

View File

@@ -0,0 +1,15 @@
export { postService } from './postService';
export { commentService } from './commentService';
export { voteService } from './voteService';
export { channelService } from './channelService';
export type { ChannelItem } from './channelService';
export { reportService, REPORT_REASONS } from './reportService';
export type { ReportReason, ReportTargetType, ReportResponse } from './reportService';
export { postSyncService } from './PostSyncService';
export type { PostDetailState, PostsState } from './PostSyncService';
export { mergePostListWithFastPath, mergeRefreshWindow } from './postMerge';

View File

@@ -0,0 +1,421 @@
/**
* 帖子服务 - API 层薄封装
* 互动操作(点赞、收藏)请使用 postSyncService
*/
import { api, PaginatedData } from '../core/api';
import { Post, CreatePostInput } from '@/types';
import { CursorPaginationRequest, CursorPaginationResponse } from '@/types/dto';
// 帖子列表响应
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[];
channel_id?: string;
}
// 更新帖子请求
interface UpdatePostRequest {
title?: string;
content?: string;
images?: string[];
}
// 帖子服务类
class PostService {
// 获取帖子列表
async getPosts(
page = 1,
pageSize = 20,
tab?: string,
channelId?: string
): Promise<PaginatedData<Post>> {
const params: Record<string, any> = {
page,
page_size: pageSize,
};
if (tab) {
params.tab = tab;
}
if (channelId) {
params.channel_id = channelId;
}
const response = await api.get<PostListResponse>('/posts', params);
return response.data;
}
// 获取关注帖子
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 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,
};
}
}
/** 上报分享成功;返回服务端最新 shares_count 供界面同步 */
async sharePost(
postId: string,
body?: { channel?: string }
): Promise<{ ok: boolean; shares_count?: number }> {
try {
const response = await api.post<{ success: boolean; shares_count: number }>(
`/posts/${postId}/share`,
body && body.channel ? { channel: body.channel } : undefined
);
if (response.code !== 0) {
return { ok: false };
}
const n = response.data?.shares_count;
return { ok: true, shares_count: typeof n === 'number' ? n : undefined };
} catch (error) {
console.error('分享帖子失败:', error);
return { ok: 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;
}
}
// ==================== 游标分页方法 ====================
/**
* 获取帖子列表(游标分页)
* GET /api/v1/posts
* @param params 游标分页请求参数(包含 post_type 可选follow, hot, latest
*/
async getPostsCursor(
params: CursorPaginationRequest = {}
): Promise<CursorPaginationResponse<Post>> {
try {
// 构建请求参数
// 后端使用 `tab` 参数区分帖子类型,使用 `cursor` 参数启用游标分页
// 始终传递 cursor 参数(空字符串表示首次请求),确保使用游标分页
const requestParams: Record<string, any> = {
cursor: params.cursor || '', // 空字符串表示首次请求
page_size: params.page_size,
};
// 将 post_type 映射为后端的 tab 参数
if (params.post_type) {
requestParams.tab = params.post_type;
}
const response = await api.get<any>('/posts', requestParams);
const data = response.data;
if (data && typeof data === 'object' && Array.isArray(data.list)) {
const isPageShape =
typeof data.page === 'number' &&
typeof data.total_pages === 'number' &&
data.next_cursor === undefined &&
data.prev_cursor === undefined;
if (isPageShape) {
const currentPage = data.page || 1;
const totalPages = data.total_pages || 1;
return {
list: data.list,
next_cursor: currentPage < totalPages ? String(currentPage + 1) : null,
prev_cursor: currentPage > 1 ? String(currentPage - 1) : null,
has_more: currentPage < totalPages,
};
}
return {
list: data.list,
next_cursor: data.next_cursor ?? null,
prev_cursor: data.prev_cursor ?? null,
has_more: data.has_more ?? false,
};
}
return {
list: [],
next_cursor: null,
prev_cursor: null,
has_more: false,
};
} catch (error) {
console.error('获取帖子列表失败:', error);
return {
list: [],
next_cursor: null,
prev_cursor: null,
has_more: false,
};
}
}
/**
* 搜索帖子(游标分页)
* GET /api/v1/posts/search
* @param query 搜索关键词
* @param params 游标分页请求参数
*/
async searchPostsCursor(
query: string,
params: CursorPaginationRequest = {}
): Promise<CursorPaginationResponse<Post>> {
try {
const response = await api.get<any>('/posts/search', {
...params,
keyword: query,
});
const data = response.data;
if (data && typeof data === 'object' && Array.isArray(data.list)) {
const isPageShape =
typeof data.page === 'number' &&
typeof data.total_pages === 'number' &&
data.next_cursor === undefined &&
data.prev_cursor === undefined;
if (isPageShape) {
const currentPage = data.page || 1;
const totalPages = data.total_pages || 1;
return {
list: data.list,
next_cursor: currentPage < totalPages ? String(currentPage + 1) : null,
prev_cursor: currentPage > 1 ? String(currentPage - 1) : null,
has_more: currentPage < totalPages,
};
}
return {
list: data.list,
next_cursor: data.next_cursor ?? null,
prev_cursor: data.prev_cursor ?? null,
has_more: data.has_more ?? false,
};
}
return {
list: [],
next_cursor: null,
prev_cursor: null,
has_more: false,
};
} catch (error) {
console.error('搜索帖子失败:', error);
return {
list: [],
next_cursor: null,
prev_cursor: null,
has_more: false,
};
}
}
/**
* 获取用户帖子列表(游标分页)
* GET /api/v1/users/:id/posts
* @param userId 用户ID
* @param params 游标分页请求参数
*/
async getUserPostsCursor(
userId: string,
params: CursorPaginationRequest = {}
): Promise<CursorPaginationResponse<Post>> {
try {
const response = await api.get<any>(
`/users/${userId}/posts`,
params
);
const data = response.data;
if (data && typeof data === 'object' && Array.isArray(data.list)) {
const isPageShape =
typeof data.page === 'number' &&
typeof data.total_pages === 'number' &&
data.next_cursor === undefined &&
data.prev_cursor === undefined;
if (isPageShape) {
const currentPage = data.page || 1;
const totalPages = data.total_pages || 1;
return {
list: data.list,
next_cursor: currentPage < totalPages ? String(currentPage + 1) : null,
prev_cursor: currentPage > 1 ? String(currentPage - 1) : null,
has_more: currentPage < totalPages,
};
}
return {
list: data.list,
next_cursor: data.next_cursor ?? null,
prev_cursor: data.prev_cursor ?? null,
has_more: data.has_more ?? false,
};
}
return {
list: [],
next_cursor: null,
prev_cursor: null,
has_more: false,
};
} catch (error) {
console.error('获取用户帖子列表失败:', error);
return {
list: [],
next_cursor: null,
prev_cursor: null,
has_more: false,
};
}
}
}
// 导出帖子服务实例
export const postService = new PostService();

View File

@@ -0,0 +1,83 @@
/**
* 举报服务
* 处理帖子、评论、消息的举报功能
*/
import { api } from '../core/api';
// 举报原因类型
export type ReportReason = 'spam' | 'inappropriate' | 'harassment' | 'misinformation' | 'other';
// 举报目标类型
export type ReportTargetType = 'post' | 'comment' | 'message';
// 创建举报请求
interface CreateReportRequest {
target_type: ReportTargetType;
target_id: string;
reason: ReportReason;
description?: string;
}
// 举报响应
export interface ReportResponse {
id: string;
reporter_id: string;
target_type: ReportTargetType;
target_id: string;
reason: ReportReason;
description?: string;
status: 'pending' | 'processing' | 'resolved' | 'rejected';
created_at: string;
}
// 举报原因选项
export const REPORT_REASONS: { value: ReportReason; label: string }[] = [
{ value: 'spam', label: '垃圾广告' },
{ value: 'inappropriate', label: '违规内容' },
{ value: 'harassment', label: '辱骂/攻击' },
{ value: 'misinformation', label: '虚假信息' },
{ value: 'other', label: '其他' },
];
// 举报服务类
class ReportService {
/**
* 创建举报
* @param targetType 举报目标类型
* @param targetId 目标ID
* @param reason 举报原因
* @param description 详细描述(可选)
*/
async createReport(
targetType: ReportTargetType,
targetId: string,
reason: ReportReason,
description?: string
): Promise<ReportResponse | null> {
try {
const request: CreateReportRequest = {
target_type: targetType,
target_id: targetId,
reason,
description,
};
const response = await api.post<ReportResponse>('/reports', request);
return response.data;
} catch (error) {
console.error('创建举报失败:', error);
return null;
}
}
/**
* 获取举报原因标签
*/
getReasonLabel(reason: ReportReason): string {
const item = REPORT_REASONS.find(r => r.value === reason);
return item?.label ?? reason;
}
}
export const reportService = new ReportService();
export default reportService;

View File

@@ -0,0 +1,114 @@
/**
* 投票服务
* 处理投票相关的API调用
*/
import { api } from '../core/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();