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

328
frontend/src/store/group.js Normal file
View File

@@ -0,0 +1,328 @@
/**
* 群聊状态 Store
*
* 管理群聊信息、群成员、入群申请,提供:
* - 当前群详情和成员列表
* - 群管理操作(邀请/踢人/禁言/设管理员/解散等)
* - WebSocket 群管理事件监听group.* 系列)
*
* 对应后端 API/api/v1/groups/*
* 对应 WS 事件group.created / group.member.join / group.member.leave / group.member.kicked
* group.info.update / group.dissolved / group.mute.update / group.role.update
* group.owner.transfer / group.join.request / group.join.approved
*/
import { defineStore } from 'pinia'
import { ref } from 'vue'
import groupApi from '@/api/group'
import wsService from '@/services/websocket'
import { useChatStore } from '@/store/chat'
export const useGroupStore = defineStore('group', () => {
/** 当前查看的群详情 */
const currentGroup = ref(null)
/** 当前群的成员列表 */
const currentMembers = ref([])
/** 当前群的入群申请列表 */
const joinRequests = ref([])
/** 群搜索结果 */
const searchResults = ref([])
const searchTotal = ref(0)
// ==================== Actions群信息 ====================
/** 获取群详情 */
const fetchGroupDetail = async (groupId) => {
const res = await groupApi.getGroupDetail(groupId)
if (res.data) {
currentGroup.value = res.data
}
return currentGroup.value
}
/** 创建群聊 */
const createGroup = async (name, memberIds, avatar = '') => {
const res = await groupApi.createGroup(name, memberIds, avatar)
return res.data
}
/** 更新群信息 */
const updateGroup = async (groupId, data) => {
await groupApi.updateGroup(groupId, data)
if (currentGroup.value && currentGroup.value.id === groupId) {
Object.assign(currentGroup.value, data)
}
}
/** 解散群聊 */
const dissolveGroup = async (groupId) => {
await groupApi.dissolveGroup(groupId)
currentGroup.value = null
}
// ==================== Actions群成员管理 ====================
/** 获取群成员列表 */
const fetchMembers = async (groupId) => {
const res = await groupApi.getMembers(groupId)
if (res.data) {
currentMembers.value = res.data.list || res.data || []
}
return currentMembers.value
}
/** 邀请入群 */
const inviteMembers = async (groupId, userIds) => {
await groupApi.inviteMembers(groupId, userIds)
}
/** 踢出群成员 */
const kickMember = async (groupId, userId) => {
await groupApi.kickMember(groupId, userId)
currentMembers.value = currentMembers.value.filter(m => m.user_id !== userId)
}
/** 退出群聊 */
const leaveGroup = async (groupId) => {
await groupApi.leaveGroup(groupId)
currentGroup.value = null
}
/** 转让群主 */
const transferOwner = async (groupId, newOwnerId) => {
await groupApi.transferOwner(groupId, newOwnerId)
}
/** 设置/取消管理员 */
const setMemberRole = async (groupId, userId, role) => {
await groupApi.setMemberRole(groupId, userId, role)
const member = currentMembers.value.find(m => m.user_id === userId)
if (member) member.role = role
}
/** 禁言/解除禁言 */
const muteMember = async (groupId, userId, isMuted) => {
await groupApi.muteMember(groupId, userId, isMuted)
const member = currentMembers.value.find(m => m.user_id === userId)
if (member) member.is_muted = isMuted
}
/** 全体禁言 */
const setAllMuted = async (groupId, isMuted) => {
await groupApi.setAllMuted(groupId, isMuted)
if (currentGroup.value && currentGroup.value.id === groupId) {
currentGroup.value.is_all_muted = isMuted
}
}
/** 修改群昵称 */
const updateNickname = async (groupId, nickname) => {
await groupApi.updateNickname(groupId, nickname)
}
/** 设置/取消免打扰 */
const setDoNotDisturb = async (conversationId, isDoNotDisturb) => {
await groupApi.setDoNotDisturb(conversationId, isDoNotDisturb)
}
// ==================== Actions入群申请 ====================
/** 提交入群申请 */
const submitJoinRequest = async (groupId, message = '') => {
await groupApi.submitJoinRequest(groupId, message)
}
/** 获取入群申请列表 */
const fetchJoinRequests = async (groupId) => {
const res = await groupApi.getJoinRequests(groupId)
if (res.data) {
joinRequests.value = res.data.list || res.data || []
}
return joinRequests.value
}
/** 审批入群申请 */
const reviewJoinRequest = async (groupId, requestId, action) => {
await groupApi.reviewJoinRequest(groupId, requestId, action)
joinRequests.value = joinRequests.value.filter(r => r.id !== requestId)
}
// ==================== Actions搜索 ====================
/**
* 搜索公开群
* @param {string} keyword - 搜索关键词
* @param {number} page - 页码
* @param {number} pageSize - 每页条数
* @param {boolean} append - 是否追加到现有列表(分页加载更多时为 true
*/
const searchGroups = async (keyword, page = 1, pageSize = 20, append = false) => {
const res = await groupApi.searchGroups(keyword, page, pageSize)
if (res.data) {
const newList = res.data.list || []
searchResults.value = append ? [...searchResults.value, ...newList] : newList
searchTotal.value = res.data.total || 0
}
return { list: searchResults.value, total: searchTotal.value }
}
// ==================== WS 事件监听 ====================
let _wsInitialized = false
/** 初始化 WebSocket 群管理事件监听(幂等) */
const initWsListeners = () => {
if (_wsInitialized) return
_wsInitialized = true
wsService.on('group.created', _onGroupCreated)
wsService.on('group.member.join', _onMemberJoin)
wsService.on('group.member.leave', _onMemberLeave)
wsService.on('group.member.kicked', _onMemberKicked)
wsService.on('group.info.update', _onInfoUpdate)
wsService.on('group.dissolved', _onDissolved)
wsService.on('group.mute.update', _onMuteUpdate)
wsService.on('group.role.update', _onRoleUpdate)
wsService.on('group.owner.transfer', _onOwnerTransfer)
wsService.on('group.join.request', _onJoinRequest)
wsService.on('group.join.approved', _onJoinApproved)
}
/** 群聊创建通知 → 刷新会话列表 */
const _onGroupCreated = () => {
const chatStore = useChatStore()
chatStore.fetchConversations()
}
/** 新成员加入 → 刷新成员和会话 */
const _onMemberJoin = (msg) => {
if (!msg || !msg.data) return
const { group_id } = msg.data
if (currentGroup.value && currentGroup.value.id === group_id) {
fetchMembers(group_id)
}
const chatStore = useChatStore()
chatStore.fetchConversations()
}
/** 成员退出 → 刷新成员 */
const _onMemberLeave = (msg) => {
if (!msg || !msg.data) return
const { group_id, user_id } = msg.data
if (currentGroup.value && currentGroup.value.id === group_id) {
currentMembers.value = currentMembers.value.filter(m => m.user_id !== user_id)
}
}
/** 成员被踢 → 刷新或跳转 */
const _onMemberKicked = (msg) => {
if (!msg || !msg.data) return
const { group_id, user_id } = msg.data
if (currentGroup.value && currentGroup.value.id === group_id) {
currentMembers.value = currentMembers.value.filter(m => m.user_id !== user_id)
}
const chatStore = useChatStore()
chatStore.fetchConversations()
}
/** 群信息变更 → 更新当前群详情 + 刷新会话 */
const _onInfoUpdate = (msg) => {
if (!msg || !msg.data) return
const { group_id, updates } = msg.data
if (currentGroup.value && currentGroup.value.id === group_id && updates) {
Object.assign(currentGroup.value, updates)
}
const chatStore = useChatStore()
chatStore.fetchConversations()
}
/** 群解散 → 清空当前群 + 刷新会话 */
const _onDissolved = (msg) => {
if (!msg || !msg.data) return
const { group_id } = msg.data
if (currentGroup.value && currentGroup.value.id === group_id) {
currentGroup.value = null
uni.showToast({ title: '该群已被解散', icon: 'none' })
}
const chatStore = useChatStore()
chatStore.fetchConversations()
}
/** 禁言变更 → 更新成员 / 群信息 */
const _onMuteUpdate = (msg) => {
if (!msg || !msg.data) return
const { group_id, user_id, is_muted, is_all_muted } = msg.data
if (currentGroup.value && currentGroup.value.id === group_id) {
if (is_all_muted !== undefined) {
currentGroup.value.is_all_muted = is_all_muted
}
if (user_id) {
const member = currentMembers.value.find(m => m.user_id === user_id)
if (member) member.is_muted = is_muted
}
}
}
/** 角色变更 → 更新成员角色 */
const _onRoleUpdate = (msg) => {
if (!msg || !msg.data) return
const { group_id, user_id, role } = msg.data
if (currentGroup.value && currentGroup.value.id === group_id) {
const member = currentMembers.value.find(m => m.user_id === user_id)
if (member) member.role = role
}
}
/** 群主转让 → 刷新群详情和成员 */
const _onOwnerTransfer = (msg) => {
if (!msg || !msg.data) return
const { group_id } = msg.data
if (currentGroup.value && currentGroup.value.id === group_id) {
fetchGroupDetail(group_id)
fetchMembers(group_id)
}
}
/** 新入群申请 → 提示通知 */
const _onJoinRequest = (msg) => {
if (!msg || !msg.data) return
uni.showToast({ title: '收到新的入群申请', icon: 'none' })
}
/** 入群申请通过 → 刷新会话列表 */
const _onJoinApproved = () => {
const chatStore = useChatStore()
chatStore.fetchConversations()
uni.showToast({ title: '入群申请已通过', icon: 'none' })
}
return {
currentGroup,
currentMembers,
joinRequests,
searchResults,
searchTotal,
fetchGroupDetail,
createGroup,
updateGroup,
dissolveGroup,
fetchMembers,
inviteMembers,
kickMember,
leaveGroup,
transferOwner,
setMemberRole,
muteMember,
setAllMuted,
updateNickname,
setDoNotDisturb,
submitJoinRequest,
fetchJoinRequests,
reviewJoinRequest,
searchGroups,
initWsListeners
}
})