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:
bujinyuan
2026-03-04 14:58:23 +08:00
parent ad76d9a613
commit 19979da59e
77 changed files with 10053 additions and 490 deletions

55
frontend/src/api/file.js Normal file
View 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
View 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
}

View File

@@ -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
}