refactor: restructure core architecture and responsive system
All checks were successful
Frontend CI / ota-android (push) Successful in 1m40s
Frontend CI / ota-ios (push) Successful in 1m39s
Frontend CI / build-and-push-web (push) Successful in 3m30s
Frontend CI / build-android-apk (push) Successful in 1h1m8s

This commit implements a major architectural refactor to improve modularity, type safety, and maintainability across the codebase.

Key changes include:
- **Responsive System Refactor**: Migrated from a monolithic `useResponsive` hook to a set of specialized, granular hooks (`useBreakpoint`, `useOrientation`, `usePlatform`, etc.) located in `src/presentation/hooks/responsive`. This reduces unnecessary re-renders and improves developer experience.
- **Core Service Restructuring**: Introduced a new directory structure for services, including dedicated folders for `datasources`, `mappers`, and domain-specific types (`message`, `post`).
- **Data Layer Improvements**:
    - Centralized JSON parsing logic in `src/database/core/jsonUtils.ts`.
    - Cleaned up repository implementations by removing redundant local utility functions.
    - Refactored `MessageMapper` to use the new centralized JSON utility.
- **Type System Cleanup**:
    - Decomposed the large `src/types/dto.ts` into a modular `src/types/dto/` directory.
    - Simplified `src/types/index.ts` and introduced backward-compatible aliases for core entities.
- **Utility Consolidation**:
    - Created a centralized `src/utils/formatTime.ts` to replace fragmented date formatting logic across various screens and components.
    - Removed deprecated responsive utility files in favor of the new hook-based system.
- **Service Logic Refinement**: Refactored `ApiClient` and `WebSocketService` to use a centralized `showVerificationModal` service, removing duplicated state management for verification prompts.
This commit is contained in:
2026-05-05 19:07:33 +08:00
parent f5f9c3a619
commit 3196972596
96 changed files with 4609 additions and 3149 deletions

View File

@@ -1,998 +0,0 @@
/**
* DTO Types - Data Transfer Object types for API communication
* 使用snake_case命名规范
*/
// ==================== User DTOs ====================
export interface UserDTO {
id: string;
username: string;
nickname: string;
avatar: string;
cover_url: string;
bio: string;
website: string;
location: string;
phone?: string;
email?: string;
email_verified?: boolean;
posts_count: number;
followers_count: number;
following_count: number;
created_at: string;
is_following?: boolean;
is_following_me?: boolean;
identity?: UserIdentity;
verification_status?: VerificationStatus;
}
export interface UserDetailDTO {
id: string;
username: string;
nickname: string;
email: string;
email_verified?: boolean;
avatar: string | null;
bio: string | null;
website: string | null;
location: string | null;
posts_count: number;
followers_count: number;
following_count: number;
is_verified: boolean;
created_at: string;
// 额外字段
is_following?: boolean; // 当前用户是否关注了该用户
is_following_me?: boolean; // 该用户是否关注了当前用户
identity?: UserIdentity;
verification_status?: VerificationStatus;
}
// ==================== Post DTOs ====================
export interface PostImageDTO {
id: string;
url: string;
thumbnail_url: string;
preview_url: string; // 列表/网格预览图
preview_url_large: string; // 详情页预览图
width: number;
height: number;
}
/** 帖子所属频道(列表卡片展示) */
export interface PostChannelBriefDTO {
id: string;
name: string;
}
export interface PostDTO {
id: string;
user_id: string;
title: string;
content: string;
segments?: MessageSegment[];
images: PostImageDTO[];
status?: string;
likes_count: number;
comments_count: number;
favorites_count: number;
shares_count: number;
views_count: number;
is_pinned: boolean;
is_locked: boolean;
is_vote: boolean;
created_at: string;
updated_at?: string;
/** 用户编辑正文/标题/图片的时间;优先用于「已编辑」展示 */
content_edited_at?: string;
author: UserDTO | null;
is_liked: boolean;
is_favorited: boolean;
// 额外字段
channel_id?: string;
/** 频道摘要(与 channel_id 对应,服务端批量填充) */
channel?: PostChannelBriefDTO | null;
top_comment?: CommentDTO | null;
}
export interface PostDetailDTO extends PostDTO {
status: string;
updated_at: string;
}
// ==================== Comment DTOs ====================
// 评论图片响应
export interface CommentImageDTO {
url: string;
}
export interface CommentDTO {
id: string;
post_id: string;
user_id: string;
parent_id: string | null;
root_id: string | null;
content: string;
segments?: MessageSegment[];
images: CommentImageDTO[];
likes_count: number;
replies_count: number;
created_at: string;
author: UserDTO;
is_liked: boolean;
target_id?: string | null; // 被回复的评论ID前端根据此ID找到被回复用户的昵称
replies?: CommentDTO[]; // 子回复列表(扁平化,所有层级都在这里)
}
// ==================== Notification DTOs ====================
export interface NotificationDTO {
id: string;
user_id: string;
type: string;
title: string;
content: string;
data: string;
is_read: boolean;
created_at: string;
}
// ==================== Chat System Types ====================
// 会话类型
export type ConversationType = 'private' | 'group';
// 消息内容类型
export type MessageContentType = 'text' | 'image' | 'video' | 'audio' | 'file';
// 消息状态
export type MessageStatus = 'normal' | 'recalled' | 'deleted';
// ==================== Message Segment Types ====================
// Segment类型
export type SegmentType = 'text' | 'image' | 'voice' | 'video' | 'file' | 'at' | 'reply' | 'face' | 'link' | 'vote' | 'post_ref';
// 文本Segment数据
export interface TextSegmentData {
content?: string; // 旧格式字段
text?: string; // 新格式字段(用于 REST API
}
// 图片Segment数据
export interface ImageSegmentData {
url: string;
width?: number;
height?: number;
thumbnail_url?: string;
file_size?: number;
}
// 语音Segment数据
export interface VoiceSegmentData {
url: string;
duration?: number; // 秒
file_size?: number;
}
// 视频Segment数据
export interface VideoSegmentData {
url: string;
width?: number;
height?: number;
duration?: number; // 秒
thumbnail_url?: string;
file_size?: number;
}
// 文件Segment数据
export interface FileSegmentData {
url: string;
name: string;
size?: number;
mime_type?: string;
}
// @Segment数据
export interface AtSegmentData {
user_id: string; // "all" 表示@所有人
nickname?: string;
}
// 回复Segment数据
export interface ReplySegmentData {
id: string; // 被回复消息的ID
seq?: number; // 被回复消息的序号(可选,用于快速查找)
}
// 表情Segment数据
export interface FaceSegmentData {
id: number;
name?: string;
url?: string;
}
// 链接Segment数据
export interface LinkSegmentData {
url: string;
title?: string;
description?: string;
image?: string;
}
// 投票选项数据
export interface VoteOptionData {
id: string;
content: string;
}
// 投票Segment数据
export interface VoteSegmentData {
options: VoteOptionData[];
max_choices?: number;
is_multi_choice?: boolean;
}
// 帖子内链Segment数据
export interface PostRefSegmentData {
post_id: string;
title?: string;
author?: {
id: string;
nickname: string;
avatar?: string;
};
status?: string;
accessible?: boolean;
click_count?: number;
}
// MessageSegment 联合类型 - 使用 discriminated union
export type MessageSegment =
| { type: 'text'; data: TextSegmentData }
| { type: 'image'; data: ImageSegmentData }
| { type: 'voice'; data: VoiceSegmentData }
| { type: 'video'; data: VideoSegmentData }
| { type: 'file'; data: FileSegmentData }
| { type: 'at'; data: AtSegmentData }
| { type: 'reply'; data: ReplySegmentData }
| { type: 'face'; data: FaceSegmentData }
| { type: 'link'; data: LinkSegmentData }
| { type: 'vote'; data: VoteSegmentData }
| { type: 'post_ref'; data: PostRefSegmentData };
// 消息响应
export interface MessageResponse {
id: string;
conversation_id: string;
sender_id: string;
seq: number;
segments: MessageSegment[];
reply_to_id?: string;
status: MessageStatus;
category?: string;
created_at: string;
sender?: UserDTO;
is_system_notice?: boolean;
notice_content?: string;
}
// 会话响应
export interface ConversationResponse {
id: string; // 雪花算法ID (使用string避免JavaScript精度丢失)
type: ConversationType;
is_pinned?: boolean;
notification_muted?: boolean;
last_seq: number;
last_message?: MessageResponse;
last_message_at: string;
unread_count: number;
participants?: UserDTO[]; // 私聊时使用
member_count?: number; // 群聊时使用
group?: GroupResponse; // 群聊会话的群组信息
my_last_read_seq?: number; // 当前用户的已读位置
other_last_read_seq?: number; // 对方用户的已读位置
created_at: string;
updated_at: string;
}
// 会话参与者响应
export interface ConversationParticipantResponse {
user_id: string;
last_read_seq: number;
notification_muted: boolean;
is_pinned: boolean;
}
// 会话详情响应
export interface ConversationDetailResponse {
id: string;
type: ConversationType;
is_pinned?: boolean;
notification_muted?: boolean;
last_seq: number;
last_message?: MessageResponse;
last_message_at: string;
unread_count: number;
participants: UserDTO[];
my_last_read_seq: number; // 当前用户的已读位置
other_last_read_seq: number; // 对方用户的已读位置
group?: GroupResponse; // 群聊会话的群组信息
created_at: string;
updated_at: string;
}
// 创建会话请求
export interface CreateConversationRequest {
user_id: string; // 目标用户ID (UUID格式)
}
// 发送消息请求
export interface SendMessageRequest {
segments?: MessageSegment[]; // 消息链(可选)
content?: string; // 纯文本内容(可选,用于兼容旧版本)
reply_to_id?: string; // 回复的消息ID (string类型)
}
// 标记已读请求
export interface MarkReadRequest {
last_read_seq: number; // 已读到的seq位置
}
// 未读数响应
export interface UnreadCountResponse {
total_unread_count: number; // 所有会话的未读总数
}
// 单个会话未读数响应
export interface ConversationUnreadCountResponse {
conversation_id: string;
unread_count: number;
}
// 会话列表响应
export interface ConversationListResponse {
list: ConversationResponse[];
total: number;
page: number;
page_size: number;
total_pages: number;
}
// 消息列表响应
export interface MessageListResponse {
messages: MessageResponse[];
total: number;
page: number;
page_size: number;
}
// 消息同步响应(增量同步)
export interface MessageSyncResponse {
messages: MessageResponse[];
has_more: boolean;
}
// ==================== Auth DTOs ====================
export interface LoginDTO {
user: UserDTO;
access_token: string;
refresh_token: string;
}
export interface RegisterDTO {
user: UserDTO;
access_token: string;
refresh_token: string;
}
export interface RefreshTokenDTO {
access_token: string;
refresh_token: string;
}
// ==================== Common DTOs ====================
export interface SuccessDTO {
success: boolean;
message: string;
}
export interface AvailableDTO {
available: boolean;
}
export interface CountDTO {
count: number;
}
export interface URLDTO {
url: string;
}
// ==================== API Response Types ====================
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;
}
// ==================== System Message Types ====================
// 消息分类
export type MessageCategory = 'chat' | 'notification' | 'announcement' | 'marketing';
// 系统消息类型
export type SystemMessageType =
| 'like_post'
| 'like_comment'
| 'like_reply'
| 'favorite_post'
| 'comment'
| 'reply'
| 'follow'
| 'mention'
| 'system'
| 'announcement'
| 'announce'
| 'group_invite'
| 'group_join_apply'
| 'group_join_approved'
| 'group_join_rejected';
// 系统消息额外数据
export interface SystemMessageExtraData {
// 后端返回的字段(使用 actor_ 前缀)
actor_id?: number; // 操作者ID (数字格式,兼容旧数据)
actor_id_str?: string; // 操作者ID (UUID字符串格式)
actor_name?: string; // 操作者名称
avatar_url?: string; // 操作者头像
target_id?: string; // 目标ID (UUID字符串格式)
target_title?: string; // 目标标题(如帖子标题)
target_type?: string; // 目标类型
action_url?: string; // 操作URL
action_time?: string; // 操作时间
group_id?: string;
group_name?: string;
group_avatar?: string;
group_description?: string;
flag?: string;
request_type?: 'invite' | 'join_apply';
request_status?: 'pending' | 'accepted' | 'rejected' | 'cancelled' | 'expired';
reason?: string;
target_user_id?: string;
target_user_name?: string;
target_user_avatar?: string;
// 兼容旧字段(使用 operator_ 前缀)
operator_id?: number; // 操作者ID
operator_name?: string; // 操作者名称
operator_avatar?: string; // 操作者头像
post_id?: number; // 帖子ID
post_title?: string; // 帖子标题
comment_id?: number; // 评论ID
comment_preview?: string; // 评论预览
reply_id?: number; // 回复ID
}
// 系统消息响应
export interface SystemMessageResponse {
id: string; // 后端返回字符串格式
sender_id: string;
receiver_id: string;
content: string;
category: MessageCategory;
system_type: SystemMessageType;
extra_data?: SystemMessageExtraData;
is_read?: boolean;
created_at: string;
}
// 系统消息列表响应
export interface SystemMessageListResponse {
messages: SystemMessageResponse[];
total: number;
has_more: boolean;
}
// 系统消息未读数响应
export interface SystemUnreadCountResponse {
unread_count: number;
}
// ==================== Push Device Types ====================
// 设备类型
export type DeviceType = 'ios' | 'android' | 'web';
// ==================== 游标分页相关 DTO ====================
/**
* 游标分页请求参数
*/
export interface CursorPaginationRequest {
cursor?: string;
direction?: 'forward' | 'backward';
page_size?: number;
post_type?: 'follow' | 'hot' | 'latest';
channel_id?: string;
}
/**
* 游标分页响应
*/
export interface CursorPaginationResponse<T> {
/** 数据项列表 */
list: T[];
/** 下一页游标 */
next_cursor: string | null;
/** 上一页游标 */
prev_cursor: string | null;
/** 是否有更多数据 */
has_more: boolean;
}
// 设备Token响应
export interface DeviceTokenResponse {
id: number;
device_id: string;
device_type: DeviceType;
is_active: boolean;
device_name?: string;
last_used_at: string;
created_at: string;
}
// 注册设备请求
export interface RegisterDeviceRequest {
device_id: string;
device_type: DeviceType;
push_token?: string;
device_name?: string;
}
// 推送记录响应
export interface PushRecordResponse {
id: number;
message_id: number;
push_channel: string;
push_status: string;
retry_count: number;
pushed_at?: string;
delivered_at?: string;
created_at: string;
}
// ==================== Group Chat Types ====================
// 群组加入类型
export type JoinType = 0 | 1 | 2; // 0:允许任何人 1:需要审批 2:不允许
// 群组成员角色
export type GroupRole = 'owner' | 'admin' | 'member';
// 群组信息
export interface GroupResponse {
/** 应用内统一为十进制字符串groupService 从 API 归一化),避免大整型精度问题 */
id: string;
name: string;
avatar: string;
description: string;
owner_id: string; // UUID字符串
member_count: number;
max_members: number;
join_type: JoinType;
mute_all: boolean;
created_at: string;
}
// 群组成员信息
export interface GroupMemberResponse {
id: number;
group_id: string;
user_id: string; // UUID字符串
role: GroupRole;
nickname: string;
muted: boolean;
join_time: string;
user?: UserDTO;
}
// 群公告信息
export interface GroupAnnouncementResponse {
id: number;
group_id: string;
content: string;
author_id: string; // UUID字符串
is_pinned: boolean;
created_at: string;
}
// 创建群组请求
export interface CreateGroupRequest {
name: string;
description?: string;
member_ids?: string[]; // UUID字符串数组
}
// 更新群组请求
export interface UpdateGroupRequest {
name?: string;
description?: string;
avatar?: string;
}
// 邀请成员请求
export interface InviteMembersRequest {
member_ids: string[]; // UUID字符串数组
}
export interface HandleGroupRequestAction {
flag: string;
approve: boolean;
reason?: string;
}
// 转让群主请求
export interface TransferOwnerRequest {
new_owner_id: string; // UUID字符串
}
// 设置成员角色请求
export interface SetRoleRequest {
role: 'admin' | 'member';
}
// 设置群昵称请求
export interface SetNicknameRequest {
nickname: string;
}
// 禁言成员请求
export interface MuteMemberRequest {
muted: boolean;
}
// 全员禁言请求
export interface SetMuteAllRequest {
mute_all: boolean;
}
// 当前用户群成员信息响应
export interface MyMemberInfoResponse {
member: GroupMemberResponse;
mute_all: boolean;
is_muted: boolean;
can_speak: boolean;
}
// 设置加群方式请求
export interface SetJoinTypeRequest {
join_type: JoinType;
}
// 设置群头像请求
export interface SetGroupAvatarRequest {
avatar: string;
}
// 创建群公告请求
export interface CreateAnnouncementRequest {
content: string;
}
// 群组列表响应
export interface GroupListResponse {
list: GroupResponse[];
total: number;
page: number;
page_size: number;
total_pages: number;
}
// 群组成员列表响应
export interface GroupMemberListResponse {
list: GroupMemberResponse[];
total: number;
page: number;
page_size: number;
total_pages: number;
}
// 群公告列表响应
export interface GroupAnnouncementListResponse {
list: GroupAnnouncementResponse[];
total: number;
page: number;
page_size: number;
total_pages: number;
}
// ==================== WS Event Types ====================
// WS 事件类型
export type WSEventType = 'message' | 'notice' | 'request' | 'meta';
// WS 消息详细类型
export type WSDetailType = 'private' | 'group' | 'follow' | 'like' | 'comment' | 'request' | 'typing' | 'heartbeat' | 'read';
// WS 事件消息段(与后端 message 字段一致)
export interface WSSegment {
type: string;
data: Record<string, any>;
}
// WS 事件(后端推送的事件格式)
export interface WSEvent {
id: string; // 事件唯一ID
time: number; // 事件时间戳(毫秒)
type: WSEventType; // 事件类型: message(消息), notice(通知), request(请求), meta(元事件)
detail_type: WSDetailType; // 详细类型: private(私聊), group(群聊), follow(关注), like(点赞)等
seq: string; // 消息序号
message?: WSSegment[]; // 消息内容数组
conversation_id: string; // 会话ID
}
// 旧的类型名称,保持向后兼容
export type SSEEventType = WSEventType;
export type SSEDetailType = WSDetailType;
export interface SSESegment extends WSSegment {}
export interface SSEEvent extends WSEvent {}
/**
* 从消息segments中提取纯文本内容
* 用于消息列表显示、通知等场景
*/
export function extractTextFromSegments(segments?: MessageSegment[], memberMap?: Map<string, { nickname: string; user_id: string }>): string {
if (!segments || segments.length === 0) {
return '';
}
let result = '';
for (const segment of segments) {
switch (segment.type) {
case 'text':
const textData = segment.data as TextSegmentData;
result += textData.content ?? textData.text ?? '';
break;
case 'at':
const atData = segment.data as AtSegmentData;
if (atData.user_id === 'all') {
result += '@所有人 ';
} else {
const member = memberMap?.get(atData.user_id);
const displayName = member?.nickname || atData.nickname || atData.user_id;
result += `@${displayName} `;
}
break;
case 'image':
result += '[图片]';
break;
case 'voice':
result += '[语音]';
break;
case 'video':
result += '[视频]';
break;
case 'file':
const fileData = segment.data as FileSegmentData;
result += `[文件: ${fileData.name || '未知'}]`;
break;
case 'face':
const faceData = segment.data as FaceSegmentData;
result += faceData.name ? `[${faceData.name}]` : '[表情]';
break;
case 'link':
const linkData = segment.data as LinkSegmentData;
result += linkData.title ? `[链接: ${linkData.title}]` : '[链接]';
break;
case 'reply':
// 回复不显示在预览中
break;
case 'post_ref':
const refData = segment.data as PostRefSegmentData;
result += refData.title ? `[帖子: ${refData.title}]` : '[帖子引用]';
break;
}
}
return result.trim();
}
export type VisibilityLevel = 'everyone' | 'following' | 'self';
export interface PrivacySettingsDTO {
followers_visibility: VisibilityLevel;
following_visibility: VisibilityLevel;
posts_visibility: VisibilityLevel;
favorites_visibility: VisibilityLevel;
}
export interface PrivacySettingsRequestDTO {
followers_visibility?: VisibilityLevel;
following_visibility?: VisibilityLevel;
posts_visibility?: VisibilityLevel;
favorites_visibility?: VisibilityLevel;
}
export interface DeletionStatusDTO {
is_pending_deletion: boolean;
deletion_requested_at?: string;
cool_down_days?: number;
}
// ==================== Vote DTOs ====================
export interface VoteOptionDTO {
id: string;
content: string;
votes_count: number;
}
export interface VoteResultDTO {
options: VoteOptionDTO[];
total_votes: number;
has_voted: boolean;
voted_option_id?: string;
}
export interface CreateVotePostRequest {
title: string;
content?: string;
images?: string[];
vote_options: Array<{
content: string;
}>;
channel_id?: string;
}
// ==================== Verification DTOs ====================
export type VerificationStatus = 'not_submitted' | 'pending' | 'approved' | 'rejected' | 'none';
export type UserIdentity = 'general' | 'student' | 'teacher' | 'staff' | 'alumni' | 'external';
export interface VerificationStatusDTO {
verification_status: VerificationStatus;
identity?: UserIdentity;
has_pending_request?: boolean;
rejected_reason?: string;
verified_at?: string;
latest_record?: {
id: string;
reject_reason?: string;
real_name?: string;
department?: string;
created_at?: string;
};
}
export interface VerificationRecordDTO {
id: string;
user_id: string;
identity: UserIdentity;
status: VerificationStatus;
real_name: string;
student_id?: string;
employee_id?: string;
department?: string;
proof_materials: string;
reviewer_id?: string;
review_note?: string;
created_at: string;
updated_at: string;
}
export interface SubmitVerificationRequest {
identity: UserIdentity;
real_name: string;
student_id?: string;
employee_id?: string;
department?: string;
proof_materials: string;
}
export interface RequestDeletionRequestDTO {
password: string;
}
export async function extractTextFromSegmentsAsync(
segments?: MessageSegment[],
getUserCache?: (userId: string) => Promise<UserDTO | null>,
getUserById?: (userId: string) => Promise<UserDTO | null>
): Promise<string> {
if (!segments || segments.length === 0) {
return '';
}
let result = '';
for (const segment of segments) {
switch (segment.type) {
case 'text':
const textData = segment.data as TextSegmentData;
result += textData.content ?? textData.text ?? '';
break;
case 'at':
const atData = segment.data as AtSegmentData;
if (atData.user_id === 'all') {
result += '@所有人 ';
} else if (atData.nickname) {
// 优先使用已有的 nickname
result += `@${atData.nickname} `;
} else {
// 异步获取用户名
let nickname = atData.user_id; // 默认显示 user_id
try {
// 先尝试从缓存获取
if (getUserCache) {
const cachedUser = await getUserCache(atData.user_id);
if (cachedUser?.nickname) {
nickname = cachedUser.nickname;
} else if (getUserById) {
// 缓存没有,从 API 获取
const user = await getUserById(atData.user_id);
if (user?.nickname) {
nickname = user.nickname;
}
}
} else if (getUserById) {
// 没有缓存函数,直接从 API 获取
const user = await getUserById(atData.user_id);
if (user?.nickname) {
nickname = user.nickname;
}
}
} catch (error) {
// 获取失败,使用 user_id 作为 fallback
console.warn('[extractTextFromSegmentsAsync] 获取用户信息失败:', error);
}
result += `@${nickname} `;
}
break;
case 'image':
result += '[图片]';
break;
case 'voice':
result += '[语音]';
break;
case 'video':
result += '[视频]';
break;
case 'file':
const fileData = segment.data as FileSegmentData;
result += `[文件: ${fileData.name || '未知'}]`;
break;
case 'face':
const faceData = segment.data as FaceSegmentData;
result += faceData.name ? `[${faceData.name}]` : '[表情]';
break;
case 'link':
const linkData = segment.data as LinkSegmentData;
result += linkData.title ? `[链接: ${linkData.title}]` : '[链接]';
break;
case 'reply':
// 回复不显示在预览中
break;
case 'post_ref':
const refDataAsync = segment.data as PostRefSegmentData;
result += refDataAsync.title ? `[帖子: ${refDataAsync.title}]` : '[帖子引用]';
break;
}
}
return result.trim();
}

210
src/types/dto/common.ts Normal file
View File

@@ -0,0 +1,210 @@
/**
* Auth, Notification & System DTOs
*/
import type { UserDTO } from './user';
import type { MessageSegment, AtSegmentData, FileSegmentData, FaceSegmentData, LinkSegmentData, PostRefSegmentData } from './message';
// ==================== Auth DTOs ====================
export interface LoginDTO {
user: UserDTO;
access_token: string;
refresh_token: string;
}
export interface RegisterDTO {
user: UserDTO;
access_token: string;
refresh_token: string;
}
export interface RefreshTokenDTO {
access_token: string;
refresh_token: string;
}
// ==================== Notification DTOs ====================
export interface NotificationDTO {
id: string;
user_id: string;
type: string;
title: string;
content: string;
data: string;
is_read: boolean;
created_at: string;
}
// ==================== System Message Types ====================
export type MessageCategory = 'chat' | 'notification' | 'announcement' | 'marketing';
export type SystemMessageType =
| 'like_post'
| 'like_comment'
| 'like_reply'
| 'favorite_post'
| 'comment'
| 'reply'
| 'follow'
| 'mention'
| 'system'
| 'announcement'
| 'announce'
| 'group_invite'
| 'group_join_apply'
| 'group_join_approved'
| 'group_join_rejected';
export interface SystemMessageExtraData {
actor_id?: number;
actor_id_str?: string;
actor_name?: string;
avatar_url?: string;
target_id?: string;
target_title?: string;
target_type?: string;
action_url?: string;
action_time?: string;
group_id?: string;
group_name?: string;
group_avatar?: string;
group_description?: string;
flag?: string;
request_type?: 'invite' | 'join_apply';
request_status?: 'pending' | 'accepted' | 'rejected' | 'cancelled' | 'expired';
reason?: string;
target_user_id?: string;
target_user_name?: string;
target_user_avatar?: string;
operator_id?: number;
operator_name?: string;
operator_avatar?: string;
post_id?: number;
post_title?: string;
comment_id?: number;
comment_preview?: string;
reply_id?: number;
}
export interface SystemMessageResponse {
id: string;
sender_id: string;
receiver_id: string;
content: string;
category: MessageCategory;
system_type: SystemMessageType;
extra_data?: SystemMessageExtraData;
is_read?: boolean;
created_at: string;
}
export interface SystemMessageListResponse {
messages: SystemMessageResponse[];
total: number;
has_more: boolean;
}
export interface SystemUnreadCountResponse {
unread_count: number;
}
// ==================== Push Device Types ====================
export type DeviceType = 'ios' | 'android' | 'web';
export interface DeviceTokenResponse {
id: number;
device_id: string;
device_type: DeviceType;
is_active: boolean;
device_name?: string;
last_used_at: string;
created_at: string;
}
export interface RegisterDeviceRequest {
device_id: string;
device_type: DeviceType;
push_token?: string;
device_name?: string;
}
export interface PushRecordResponse {
id: number;
message_id: number;
push_channel: string;
push_status: string;
retry_count: number;
pushed_at?: string;
delivered_at?: string;
created_at: string;
}
// ==================== WS Event Types ====================
export type WSEventType = 'message' | 'notice' | 'request' | 'meta';
export type WSDetailType = 'private' | 'group' | 'follow' | 'like' | 'comment' | 'request' | 'typing' | 'heartbeat' | 'read';
export interface WSSegment {
type: string;
data: Record<string, any>;
}
export interface WSEvent {
id: string;
time: number;
type: WSEventType;
detail_type: WSDetailType;
seq: string;
message?: WSSegment[];
conversation_id: string;
}
// ==================== Verification DTOs ====================
import type { VerificationStatus, UserIdentity } from './user';
export type { VerificationStatus, UserIdentity } from './user';
export interface VerificationStatusDTO {
verification_status: VerificationStatus;
identity?: UserIdentity;
has_pending_request?: boolean;
rejected_reason?: string;
verified_at?: string;
latest_record?: {
id: string;
reject_reason?: string;
real_name?: string;
department?: string;
created_at?: string;
};
}
export interface VerificationRecordDTO {
id: string;
user_id: string;
identity: UserIdentity;
status: VerificationStatus;
real_name: string;
student_id?: string;
employee_id?: string;
department?: string;
proof_materials: string;
reviewer_id?: string;
review_note?: string;
created_at: string;
updated_at: string;
}
export interface SubmitVerificationRequest {
identity: UserIdentity;
real_name: string;
student_id?: string;
employee_id?: string;
department?: string;
proof_materials: string;
}

126
src/types/dto/group.ts Normal file
View File

@@ -0,0 +1,126 @@
/**
* Group Chat DTOs
*/
import type { UserDTO } from './user';
export type JoinType = 0 | 1 | 2;
export type GroupRole = 'owner' | 'admin' | 'member';
export interface GroupResponse {
id: string;
name: string;
avatar: string;
description: string;
owner_id: string;
member_count: number;
max_members: number;
join_type: JoinType;
mute_all: boolean;
created_at: string;
}
export interface GroupMemberResponse {
id: number;
group_id: string;
user_id: string;
role: GroupRole;
nickname: string;
muted: boolean;
join_time: string;
user?: UserDTO;
}
export interface GroupAnnouncementResponse {
id: number;
group_id: string;
content: string;
author_id: string;
is_pinned: boolean;
created_at: string;
}
export interface CreateGroupRequest {
name: string;
description?: string;
member_ids?: string[];
}
export interface UpdateGroupRequest {
name?: string;
description?: string;
avatar?: string;
}
export interface InviteMembersRequest {
member_ids: string[];
}
export interface HandleGroupRequestAction {
flag: string;
approve: boolean;
reason?: string;
}
export interface TransferOwnerRequest {
new_owner_id: string;
}
export interface SetRoleRequest {
role: 'admin' | 'member';
}
export interface SetNicknameRequest {
nickname: string;
}
export interface MuteMemberRequest {
muted: boolean;
}
export interface SetMuteAllRequest {
mute_all: boolean;
}
export interface MyMemberInfoResponse {
member: GroupMemberResponse;
mute_all: boolean;
is_muted: boolean;
can_speak: boolean;
}
export interface SetJoinTypeRequest {
join_type: JoinType;
}
export interface SetGroupAvatarRequest {
avatar: string;
}
export interface CreateAnnouncementRequest {
content: string;
}
export interface GroupListResponse {
list: GroupResponse[];
total: number;
page: number;
page_size: number;
total_pages: number;
}
export interface GroupMemberListResponse {
list: GroupMemberResponse[];
total: number;
page: number;
page_size: number;
total_pages: number;
}
export interface GroupAnnouncementListResponse {
list: GroupAnnouncementResponse[];
total: number;
page: number;
page_size: number;
total_pages: number;
}

11
src/types/dto/index.ts Normal file
View File

@@ -0,0 +1,11 @@
/**
* DTO Types - Re-exports from individual modules
*/
export * from './user';
export * from './post';
export * from './message';
export * from './group';
export * from './common';
export * from './pagination';
export * from './segmentUtils';

211
src/types/dto/message.ts Normal file
View File

@@ -0,0 +1,211 @@
/**
* Message Segment Types & Chat DTOs
*/
import type { UserDTO } from './user';
import type { GroupResponse } from './group';
export type ConversationType = 'private' | 'group';
export type MessageContentType = 'text' | 'image' | 'video' | 'audio' | 'file';
export type MessageStatus = 'normal' | 'recalled' | 'deleted';
// ==================== Segment Types ====================
export type SegmentType = 'text' | 'image' | 'voice' | 'video' | 'file' | 'at' | 'reply' | 'face' | 'link' | 'vote' | 'post_ref';
export interface TextSegmentData {
content?: string;
text?: string;
}
export interface ImageSegmentData {
url: string;
width?: number;
height?: number;
thumbnail_url?: string;
file_size?: number;
}
export interface VoiceSegmentData {
url: string;
duration?: number;
file_size?: number;
}
export interface VideoSegmentData {
url: string;
width?: number;
height?: number;
duration?: number;
thumbnail_url?: string;
file_size?: number;
}
export interface FileSegmentData {
url: string;
name: string;
size?: number;
mime_type?: string;
}
export interface AtSegmentData {
user_id: string;
nickname?: string;
}
export interface ReplySegmentData {
id: string;
seq?: number;
}
export interface FaceSegmentData {
id: number;
name?: string;
url?: string;
}
export interface LinkSegmentData {
url: string;
title?: string;
description?: string;
image?: string;
}
export interface VoteOptionData {
id: string;
content: string;
}
export interface VoteSegmentData {
options: VoteOptionData[];
max_choices?: number;
is_multi_choice?: boolean;
}
export interface PostRefSegmentData {
post_id: string;
title?: string;
author?: {
id: string;
nickname: string;
avatar?: string;
};
status?: string;
accessible?: boolean;
click_count?: number;
}
export type MessageSegment =
| { type: 'text'; data: TextSegmentData }
| { type: 'image'; data: ImageSegmentData }
| { type: 'voice'; data: VoiceSegmentData }
| { type: 'video'; data: VideoSegmentData }
| { type: 'file'; data: FileSegmentData }
| { type: 'at'; data: AtSegmentData }
| { type: 'reply'; data: ReplySegmentData }
| { type: 'face'; data: FaceSegmentData }
| { type: 'link'; data: LinkSegmentData }
| { type: 'vote'; data: VoteSegmentData }
| { type: 'post_ref'; data: PostRefSegmentData };
// ==================== Chat DTOs ====================
export interface MessageResponse {
id: string;
conversation_id: string;
sender_id: string;
seq: number;
segments: MessageSegment[];
reply_to_id?: string;
status: MessageStatus;
category?: string;
created_at: string;
sender?: UserDTO;
is_system_notice?: boolean;
notice_content?: string;
}
export interface ConversationResponse {
id: string;
type: ConversationType;
is_pinned?: boolean;
notification_muted?: boolean;
last_seq: number;
last_message?: MessageResponse;
last_message_at: string;
unread_count: number;
participants?: UserDTO[];
member_count?: number;
group?: GroupResponse;
my_last_read_seq?: number;
other_last_read_seq?: number;
created_at: string;
updated_at: string;
}
export interface ConversationParticipantResponse {
user_id: string;
last_read_seq: number;
notification_muted: boolean;
is_pinned: boolean;
}
export interface ConversationDetailResponse {
id: string;
type: ConversationType;
is_pinned?: boolean;
notification_muted?: boolean;
last_seq: number;
last_message?: MessageResponse;
last_message_at: string;
unread_count: number;
participants: UserDTO[];
my_last_read_seq: number;
other_last_read_seq: number;
group?: GroupResponse;
created_at: string;
updated_at: string;
}
export interface CreateConversationRequest {
user_id: string;
}
export interface SendMessageRequest {
segments?: MessageSegment[];
content?: string;
reply_to_id?: string;
}
export interface MarkReadRequest {
last_read_seq: number;
}
export interface UnreadCountResponse {
total_unread_count: number;
}
export interface ConversationUnreadCountResponse {
conversation_id: string;
unread_count: number;
}
export interface ConversationListResponse {
list: ConversationResponse[];
total: number;
page: number;
page_size: number;
total_pages: number;
}
export interface MessageListResponse {
messages: MessageResponse[];
total: number;
page: number;
page_size: number;
}
export interface MessageSyncResponse {
messages: MessageResponse[];
has_more: boolean;
}

View File

@@ -0,0 +1,92 @@
/**
* Pagination, Vote, Privacy & Other DTOs
*/
// ==================== Common ====================
export interface SuccessDTO {
success: boolean;
message: string;
}
export interface AvailableDTO {
available: boolean;
}
export interface CountDTO {
count: number;
}
export interface URLDTO {
url: string;
}
// ==================== Cursor Pagination ====================
export interface CursorPaginationRequest {
cursor?: string;
direction?: 'forward' | 'backward';
page_size?: number;
post_type?: 'follow' | 'hot' | 'latest';
channel_id?: string;
}
export interface CursorPaginationResponse<T> {
list: T[];
next_cursor: string | null;
prev_cursor: string | null;
has_more: boolean;
}
// ==================== Vote DTOs ====================
export interface VoteOptionDTO {
id: string;
content: string;
votes_count: number;
}
export interface VoteResultDTO {
options: VoteOptionDTO[];
total_votes: number;
has_voted: boolean;
voted_option_id?: string;
}
export interface CreateVotePostRequest {
title: string;
content?: string;
images?: string[];
vote_options: Array<{
content: string;
}>;
channel_id?: string;
}
// ==================== Privacy & Account ====================
export type VisibilityLevel = 'everyone' | 'following' | 'self';
export interface PrivacySettingsDTO {
followers_visibility: VisibilityLevel;
following_visibility: VisibilityLevel;
posts_visibility: VisibilityLevel;
favorites_visibility: VisibilityLevel;
}
export interface PrivacySettingsRequestDTO {
followers_visibility?: VisibilityLevel;
following_visibility?: VisibilityLevel;
posts_visibility?: VisibilityLevel;
favorites_visibility?: VisibilityLevel;
}
export interface DeletionStatusDTO {
is_pending_deletion: boolean;
deletion_requested_at?: string;
cool_down_days?: number;
}
export interface RequestDeletionRequestDTO {
password: string;
}

75
src/types/dto/post.ts Normal file
View File

@@ -0,0 +1,75 @@
/**
* Post & Comment DTOs
*/
import type { UserDTO } from './user';
import type { MessageSegment } from './message';
export interface PostImageDTO {
id: string;
url: string;
thumbnail_url: string;
preview_url: string;
preview_url_large: string;
width: number;
height: number;
}
export interface PostChannelBriefDTO {
id: string;
name: string;
}
export interface PostDTO {
id: string;
user_id: string;
title: string;
content: string;
segments?: MessageSegment[];
images: PostImageDTO[];
status?: string;
likes_count: number;
comments_count: number;
favorites_count: number;
shares_count: number;
views_count: number;
is_pinned: boolean;
is_locked: boolean;
is_vote: boolean;
created_at: string;
updated_at?: string;
content_edited_at?: string;
author: UserDTO | null;
is_liked: boolean;
is_favorited: boolean;
channel_id?: string;
channel?: PostChannelBriefDTO | null;
top_comment?: CommentDTO | null;
}
export interface PostDetailDTO extends PostDTO {
status: string;
updated_at: string;
}
export interface CommentImageDTO {
url: string;
}
export interface CommentDTO {
id: string;
post_id: string;
user_id: string;
parent_id: string | null;
root_id: string | null;
content: string;
segments?: MessageSegment[];
images: CommentImageDTO[];
likes_count: number;
replies_count: number;
created_at: string;
author: UserDTO;
is_liked: boolean;
target_id?: string | null;
replies?: CommentDTO[];
}

View File

@@ -0,0 +1,108 @@
/**
* Segment text extraction utilities
*/
import type { MessageSegment, TextSegmentData, AtSegmentData, FileSegmentData, FaceSegmentData, LinkSegmentData, PostRefSegmentData } from './message';
import type { UserDTO } from './user';
/**
* Extract plain text from message segments for display
*/
export function extractTextFromSegments(segments?: MessageSegment[], memberMap?: Map<string, { nickname: string; user_id: string }>): string {
if (!segments || segments.length === 0) {
return '';
}
let result = '';
for (const segment of segments) {
switch (segment.type) {
case 'text':
const textData = segment.data as TextSegmentData;
result += textData.content ?? textData.text ?? '';
break;
case 'at':
const atData = segment.data as AtSegmentData;
if (atData.user_id === 'all') {
result += '@所有人 ';
} else {
const member = memberMap?.get(atData.user_id);
const displayName = member?.nickname || atData.nickname || atData.user_id;
result += `@${displayName} `;
}
break;
case 'image':
result += '[图片]';
break;
case 'voice':
result += '[语音]';
break;
case 'video':
result += '[视频]';
break;
case 'file':
const fileData = segment.data as FileSegmentData;
result += `[文件: ${fileData.name || '未知'}]`;
break;
case 'face':
const faceData = segment.data as FaceSegmentData;
result += faceData.name ? `[${faceData.name}]` : '[表情]';
break;
case 'link':
const linkData = segment.data as LinkSegmentData;
result += linkData.title ? `[链接: ${linkData.title}]` : '[链接]';
break;
case 'reply':
break;
case 'post_ref':
const refData = segment.data as PostRefSegmentData;
result += refData.title ? `[帖子: ${refData.title}]` : '[帖子引用]';
break;
}
}
return result.trim();
}
/**
* Async version that resolves missing @at nicknames
*/
export async function extractTextFromSegmentsAsync(
segments?: MessageSegment[],
getUserCache?: (userId: string) => Promise<UserDTO | null>,
getUserById?: (userId: string) => Promise<UserDTO | null>
): Promise<string> {
if (!segments || segments.length === 0) {
return '';
}
const resolvedSegments = [...segments];
for (let i = 0; i < resolvedSegments.length; i++) {
const segment = resolvedSegments[i];
if (segment.type === 'at') {
const atData = segment.data as AtSegmentData;
if (atData.user_id !== 'all' && !atData.nickname) {
try {
let nickname = atData.user_id;
if (getUserCache) {
const cachedUser = await getUserCache(atData.user_id);
if (cachedUser?.nickname) {
nickname = cachedUser.nickname;
} else if (getUserById) {
const user = await getUserById(atData.user_id);
if (user?.nickname) nickname = user.nickname;
}
} else if (getUserById) {
const user = await getUserById(atData.user_id);
if (user?.nickname) nickname = user.nickname;
}
resolvedSegments[i] = { ...segment, data: { ...atData, nickname } };
} catch (error) {
console.warn('[extractTextFromSegmentsAsync] 获取用户信息失败:', error);
}
}
}
}
return extractTextFromSegments(resolvedSegments);
}

49
src/types/dto/user.ts Normal file
View File

@@ -0,0 +1,49 @@
/**
* User DTOs
*/
export type VerificationStatus = 'not_submitted' | 'pending' | 'approved' | 'rejected' | 'none';
export type UserIdentity = 'general' | 'student' | 'teacher' | 'staff' | 'alumni' | 'external';
export interface UserDTO {
id: string;
username: string;
nickname: string;
avatar: string;
cover_url: string;
bio: string;
website: string;
location: string;
phone?: string;
email?: string;
email_verified?: boolean;
posts_count: number;
followers_count: number;
following_count: number;
created_at: string;
is_following?: boolean;
is_following_me?: boolean;
identity?: UserIdentity;
verification_status?: VerificationStatus;
}
export interface UserDetailDTO {
id: string;
username: string;
nickname: string;
email: string;
email_verified?: boolean;
avatar: string | null;
bio: string | null;
website: string | null;
location: string | null;
posts_count: number;
followers_count: number;
following_count: number;
is_verified: boolean;
created_at: string;
is_following?: boolean;
is_following_me?: boolean;
identity?: UserIdentity;
verification_status?: VerificationStatus;
}

View File

@@ -1,73 +1,30 @@
// ============================================
// WithYou - Core Type Definitions
// 使用后端DTO类型保持snake_case命名规范
// ============================================
/**
* WithYou - Core Type Definitions
*/
// 导出DTO类型作为主要类型
export * from './dto';
export * from './schedule';
export * from './material';
export * from './trade';
// 兼容旧类型(用于内部组件)
import type {
UserDTO,
PostDTO,
PostImageDTO,
// 新的聊天系统类型
ConversationType,
MessageContentType,
MessageStatus,
MessageResponse,
ConversationResponse,
ConversationParticipantResponse,
ConversationDetailResponse,
CreateConversationRequest,
SendMessageRequest,
MarkReadRequest,
UnreadCountResponse,
ConversationUnreadCountResponse,
ConversationListResponse,
MessageListResponse,
MessageSyncResponse,
// Segment 类型
SegmentType,
MessageSegment,
TextSegmentData,
ImageSegmentData,
VoiceSegmentData,
VideoSegmentData,
FileSegmentData,
AtSegmentData,
ReplySegmentData,
FaceSegmentData,
LinkSegmentData,
VoteSegmentData,
VoteOptionData,
ConversationResponse,
MessageResponse,
} from './dto';
// ============================================
// User Types
// Backward-compat aliases — prefer DTO names directly
// ============================================
export type User = UserDTO;
export interface UserProfile extends User {
favorites_count: number;
liked_posts_count: number;
}
// ============================================
// Post Types
// ============================================
export type Post = PostDTO;
export type PostDetail = PostDTO;
export type PostImage = PostImageDTO;
// ============================================
// Vote Types
// ============================================
export type Conversation = ConversationResponse;
export type Message = MessageResponse;
export type { VoteOptionDTO as VoteOption, VoteResultDTO as VoteResult, CreateVotePostRequest as CreateVotePostReq } from './dto';
@@ -83,7 +40,6 @@ export interface CreatePostInput {
// Comment Types
// ============================================
// 评论图片
export interface CommentImage {
url: string;
}
@@ -102,7 +58,7 @@ export interface Comment {
is_liked?: boolean;
author: User;
replies?: Comment[];
target_id?: string | null; // 被回复的评论ID前端根据此ID找到被回复用户的昵称
target_id?: string | null;
created_at: string;
}
@@ -110,7 +66,7 @@ export interface CreateCommentInput {
postId: string;
parentId?: string;
content: string;
images?: string[]; // 图片URL列表
images?: string[];
replyToUserId?: string;
}
@@ -149,49 +105,4 @@ export interface NotificationBadge {
comments: number;
follows: number;
system: number;
}
// ============================================
// Message Types (New Chat System)
// ============================================
/**
* 新版会话类型 - 使用雪花算法ID和seq机制
*/
export type Conversation = ConversationResponse;
/**
* 新版消息类型 - 使用雪花算法ID和seq机制
*/
export type Message = MessageResponse;
// ============================================
// API Response Types
// ============================================
export interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
}
export interface PaginatedResponse<T> {
success: boolean;
data: T[];
pagination: {
page: number;
limit: number;
total: number;
hasMore: boolean;
nextCursor?: string;
};
}
export interface ApiError {
success: false;
error: {
code: string;
message: string;
details?: Record<string, string>;
};
}
}