feat(phase2c): 群聊与已读回执功能完整实现
Phase 2c 全部 14 个 Task 完成,包含: 后端(Go): - MinIO 文件存储服务集成(Docker + Go SDK + 通用上传 API) - Group 模块完整实现(DAO + Service + Controller + Router + Wire) - 18 个群管理 REST API + 11 个 WS 群事件推送 - 群创建/解散/邀请/踢人/退出/转让群主/设管理员/禁言/全体禁言/群公告/群昵称/免打扰/搜索 - IM Service 扩展(群消息发送/撤回 + @提醒 + 管理员无时限撤回) - 已读回执后端(单聊会话级 last_read_msg_id + 群聊消息级 im_message_reads) - 管理端群组管理(列表/详情/解散) - 数据库迁移(3 张新表 + 2 张表字段扩展) 前端(uni-app): - 群聊 Store + API 封装 + 11 个 WS 事件监听 - 已读回执 UI(单聊已读/未读标记 + 群聊 X人已读 + 已读详情页) - 7 个群聊页面(对话/创建/设置/成员/邀请/审批/搜索) - 会话列表改造(全部/单聊/群聊 Tab + @标记 + 免打扰标识) 管理端(Vue 3 + Element Plus): - 群组列表页(搜索/分页/详情弹窗/解散群聊) - 侧边栏群组管理入口 文档同步:进度/架构/设计/API/规范文档全部更新 Made-with: Cursor
This commit is contained in:
55
frontend/src/api/file.js
Normal file
55
frontend/src/api/file.js
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 文件上传模块 API
|
||||
*
|
||||
* 对应后端路由:POST /api/v1/upload
|
||||
* 使用 uni.uploadFile 上传文件到 MinIO
|
||||
*/
|
||||
|
||||
import { BASE_URL } from '@/utils/request'
|
||||
import { getToken } from '@/utils/storage'
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* @param {string} filePath - 本地文件路径(如从 uni.chooseImage 获取)
|
||||
* @returns {Promise<Object>} 返回 { url: '...' }
|
||||
*/
|
||||
const uploadFile = (filePath) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = getToken()
|
||||
uni.uploadFile({
|
||||
url: `${BASE_URL}/api/v1/upload`,
|
||||
filePath,
|
||||
name: 'file',
|
||||
header: {
|
||||
Authorization: token ? `Bearer ${token}` : ''
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
try {
|
||||
const data = JSON.parse(res.data)
|
||||
if (data.code === 0) {
|
||||
resolve(data)
|
||||
} else {
|
||||
uni.showToast({ title: data.message || '上传失败', icon: 'none' })
|
||||
reject(data)
|
||||
}
|
||||
} catch {
|
||||
uni.showToast({ title: '响应解析失败', icon: 'none' })
|
||||
reject({ code: -1, message: '响应解析失败' })
|
||||
}
|
||||
} else {
|
||||
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||
reject({ code: res.statusCode, message: '上传失败' })
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.showToast({ title: '网络异常', icon: 'none' })
|
||||
reject({ code: -1, message: err.errMsg || '网络异常' })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
uploadFile
|
||||
}
|
||||
119
frontend/src/api/group.js
Normal file
119
frontend/src/api/group.js
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 群聊模块 API
|
||||
*
|
||||
* 对应后端路由:/api/v1/groups/*
|
||||
* 群消息发送/撤回通过 WebSocket 进行,此处仅包含 REST API
|
||||
*/
|
||||
|
||||
import { get, post, put, del } from '@/utils/request'
|
||||
|
||||
/** 创建群聊 */
|
||||
const createGroup = (name, memberIds, avatar = '') => {
|
||||
return post('/api/v1/groups', { name, member_ids: memberIds, avatar })
|
||||
}
|
||||
|
||||
/** 获取群详情 */
|
||||
const getGroupDetail = (groupId) => {
|
||||
return get(`/api/v1/groups/${groupId}`)
|
||||
}
|
||||
|
||||
/** 更新群信息(名称/头像/公告/可搜索性) */
|
||||
const updateGroup = (groupId, data) => {
|
||||
return put(`/api/v1/groups/${groupId}`, data)
|
||||
}
|
||||
|
||||
/** 解散群聊 */
|
||||
const dissolveGroup = (groupId) => {
|
||||
return del(`/api/v1/groups/${groupId}`)
|
||||
}
|
||||
|
||||
/** 获取群成员列表 */
|
||||
const getMembers = (groupId) => {
|
||||
return get(`/api/v1/groups/${groupId}/members`)
|
||||
}
|
||||
|
||||
/** 邀请用户入群 */
|
||||
const inviteMembers = (groupId, userIds) => {
|
||||
return post(`/api/v1/groups/${groupId}/members`, { user_ids: userIds })
|
||||
}
|
||||
|
||||
/** 踢出群成员 */
|
||||
const kickMember = (groupId, userId) => {
|
||||
return del(`/api/v1/groups/${groupId}/members/${userId}`)
|
||||
}
|
||||
|
||||
/** 退出群聊 */
|
||||
const leaveGroup = (groupId) => {
|
||||
return del(`/api/v1/groups/${groupId}/members/me`)
|
||||
}
|
||||
|
||||
/** 转让群主 */
|
||||
const transferOwner = (groupId, newOwnerId) => {
|
||||
return put(`/api/v1/groups/${groupId}/transfer`, { new_owner_id: newOwnerId })
|
||||
}
|
||||
|
||||
/** 设置/取消管理员 */
|
||||
const setMemberRole = (groupId, userId, role) => {
|
||||
return put(`/api/v1/groups/${groupId}/members/${userId}/role`, { role })
|
||||
}
|
||||
|
||||
/** 禁言/解除禁言成员 */
|
||||
const muteMember = (groupId, userId, isMuted) => {
|
||||
return put(`/api/v1/groups/${groupId}/members/${userId}/mute`, { is_muted: isMuted })
|
||||
}
|
||||
|
||||
/** 设置/取消全体禁言 */
|
||||
const setAllMuted = (groupId, isMuted) => {
|
||||
return put(`/api/v1/groups/${groupId}/mute`, { is_muted: isMuted })
|
||||
}
|
||||
|
||||
/** 修改群内昵称 */
|
||||
const updateNickname = (groupId, nickname) => {
|
||||
return put(`/api/v1/groups/${groupId}/members/me/nickname`, { nickname })
|
||||
}
|
||||
|
||||
/** 提交入群申请 */
|
||||
const submitJoinRequest = (groupId, message = '') => {
|
||||
return post(`/api/v1/groups/${groupId}/join-requests`, { message })
|
||||
}
|
||||
|
||||
/** 获取入群申请列表(群主/管理员) */
|
||||
const getJoinRequests = (groupId) => {
|
||||
return get(`/api/v1/groups/${groupId}/join-requests`)
|
||||
}
|
||||
|
||||
/** 审批入群申请 */
|
||||
const reviewJoinRequest = (groupId, requestId, action) => {
|
||||
return put(`/api/v1/groups/${groupId}/join-requests/${requestId}`, { action })
|
||||
}
|
||||
|
||||
/** 搜索公开群 */
|
||||
const searchGroups = (keyword, page = 1, pageSize = 20) => {
|
||||
return get('/api/v1/groups/search', { keyword, page, page_size: pageSize })
|
||||
}
|
||||
|
||||
/** 设置/取消免打扰 */
|
||||
const setDoNotDisturb = (conversationId, isDoNotDisturb) => {
|
||||
return put(`/api/v1/im/conversations/${conversationId}/dnd`, { is_do_not_disturb: isDoNotDisturb })
|
||||
}
|
||||
|
||||
export default {
|
||||
createGroup,
|
||||
getGroupDetail,
|
||||
updateGroup,
|
||||
dissolveGroup,
|
||||
getMembers,
|
||||
inviteMembers,
|
||||
kickMember,
|
||||
leaveGroup,
|
||||
transferOwner,
|
||||
setMemberRole,
|
||||
muteMember,
|
||||
setAllMuted,
|
||||
updateNickname,
|
||||
submitJoinRequest,
|
||||
getJoinRequests,
|
||||
reviewJoinRequest,
|
||||
searchGroups,
|
||||
setDoNotDisturb
|
||||
}
|
||||
@@ -46,6 +46,11 @@ const getTotalUnread = () => {
|
||||
return get('/api/v1/im/unread')
|
||||
}
|
||||
|
||||
/** 获取消息已读详情(已读/未读用户列表) */
|
||||
const getMessageReadDetail = (messageId, page = 1, pageSize = 50) => {
|
||||
return get(`/api/v1/im/messages/${messageId}/reads`, { page, page_size: pageSize })
|
||||
}
|
||||
|
||||
export default {
|
||||
getConversations,
|
||||
getHistoryMessages,
|
||||
@@ -53,5 +58,6 @@ export default {
|
||||
deleteConversation,
|
||||
clearHistory,
|
||||
searchMessages,
|
||||
getTotalUnread
|
||||
getTotalUnread,
|
||||
getMessageReadDetail
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user