1372 lines
44 KiB
Go
1372 lines
44 KiB
Go
// Copyright © 2023 OpenIM. All rights reserved.
|
||
//
|
||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||
// you may not use this file except in compliance with the License.
|
||
// You may obtain a copy of the License at
|
||
//
|
||
// http://www.apache.org/licenses/LICENSE-2.0
|
||
//
|
||
// Unless required by applicable law or agreed to in writing, software
|
||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
// See the License for the specific language governing permissions and
|
||
// limitations under the License.
|
||
|
||
package api
|
||
|
||
import (
|
||
"context"
|
||
"crypto/md5"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"fmt"
|
||
"math/big"
|
||
"math/rand"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"go.mongodb.org/mongo-driver/mongo"
|
||
|
||
"git.imall.cloud/openim/open-im-server-deploy/pkg/apistruct"
|
||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/webhook"
|
||
"git.imall.cloud/openim/open-im-server-deploy/pkg/rpcli"
|
||
"git.imall.cloud/openim/protocol/constant"
|
||
"git.imall.cloud/openim/protocol/group"
|
||
"git.imall.cloud/openim/protocol/sdkws"
|
||
"git.imall.cloud/openim/protocol/wrapperspb"
|
||
"github.com/openimsdk/tools/apiresp"
|
||
"github.com/openimsdk/tools/errs"
|
||
"github.com/openimsdk/tools/log"
|
||
"github.com/openimsdk/tools/mcontext"
|
||
"github.com/openimsdk/tools/utils/datautil"
|
||
"github.com/openimsdk/tools/utils/idutil"
|
||
"github.com/openimsdk/tools/utils/timeutil"
|
||
)
|
||
|
||
type MeetingApi struct {
|
||
meetingDB database.Meeting
|
||
meetingCheckInDB database.MeetingCheckIn
|
||
groupClient *rpcli.GroupClient
|
||
userClient *rpcli.UserClient
|
||
conversationClient *rpcli.ConversationClient
|
||
systemConfigDB database.SystemConfig
|
||
}
|
||
|
||
func NewMeetingApi(meetingDB database.Meeting, meetingCheckInDB database.MeetingCheckIn, groupClient *rpcli.GroupClient, userClient *rpcli.UserClient, conversationClient *rpcli.ConversationClient, systemConfigDB database.SystemConfig) *MeetingApi {
|
||
return &MeetingApi{
|
||
meetingDB: meetingDB,
|
||
meetingCheckInDB: meetingCheckInDB,
|
||
groupClient: groupClient,
|
||
userClient: userClient,
|
||
conversationClient: conversationClient,
|
||
systemConfigDB: systemConfigDB,
|
||
}
|
||
}
|
||
|
||
// IsNotFound 检查错误是否是记录不存在
|
||
func (m *MeetingApi) IsNotFound(err error) bool {
|
||
return errs.ErrRecordNotFound.Is(err) || errs.Unwrap(err) == mongo.ErrNoDocuments
|
||
}
|
||
|
||
// loadAnchorUsers 加载主播用户信息
|
||
func (m *MeetingApi) loadAnchorUsers(ctx context.Context, anchorUserIDs []string) []*sdkws.UserInfo {
|
||
if len(anchorUserIDs) == 0 {
|
||
return nil
|
||
}
|
||
users, err := m.userClient.GetUsersInfo(ctx, anchorUserIDs)
|
||
if err != nil {
|
||
log.ZWarn(ctx, "loadAnchorUsers: failed to get anchor users info", err, "anchorUserIDs", anchorUserIDs)
|
||
return nil
|
||
}
|
||
return users
|
||
}
|
||
|
||
// genMeetingPassword 生成6位数字密码
|
||
func (m *MeetingApi) genMeetingPassword() string {
|
||
return fmt.Sprintf("%06d", rand.Intn(1000000))
|
||
}
|
||
|
||
// genMeetingID 生成会议ID
|
||
func (m *MeetingApi) genMeetingID(ctx context.Context, meetingID string) (string, error) {
|
||
if meetingID != "" {
|
||
_, err := m.meetingDB.Take(ctx, meetingID)
|
||
if err == nil {
|
||
return "", errs.ErrArgs.WrapMsg("meeting id already exists: " + meetingID)
|
||
}
|
||
// 如果记录不存在,说明可以使用这个ID
|
||
if m.IsNotFound(err) {
|
||
return meetingID, nil
|
||
}
|
||
// 其他错误直接返回
|
||
return "", err
|
||
}
|
||
// 生成唯一ID
|
||
for i := 0; i < 10; i++ {
|
||
opID := mcontext.GetOperationID(ctx)
|
||
timestamp := time.Now().UnixNano()
|
||
random := rand.Int()
|
||
data := fmt.Sprintf("%s,%d,%d", opID, timestamp, random)
|
||
hash := md5.Sum([]byte(data))
|
||
id := hex.EncodeToString(hash[:])[:16]
|
||
bi := big.NewInt(0)
|
||
bi.SetString(id, 16)
|
||
id = bi.String()
|
||
_, err := m.meetingDB.Take(ctx, id)
|
||
if err == nil {
|
||
// ID已存在,继续生成下一个
|
||
continue
|
||
}
|
||
// 如果记录不存在,说明可以使用这个ID
|
||
if m.IsNotFound(err) {
|
||
return id, nil
|
||
}
|
||
// 其他错误直接返回
|
||
return "", err
|
||
}
|
||
return "", errs.ErrInternalServer.WrapMsg("failed to generate meeting id")
|
||
}
|
||
|
||
// paginationWrapper 实现 pagination.Pagination 接口
|
||
type meetingPaginationWrapper struct {
|
||
pageNumber int32
|
||
showNumber int32
|
||
}
|
||
|
||
func (p *meetingPaginationWrapper) GetPageNumber() int32 {
|
||
if p.pageNumber <= 0 {
|
||
return 1
|
||
}
|
||
return p.pageNumber
|
||
}
|
||
|
||
func (p *meetingPaginationWrapper) GetShowNumber() int32 {
|
||
if p.showNumber <= 0 {
|
||
return 20
|
||
}
|
||
return p.showNumber
|
||
}
|
||
|
||
// CreateMeeting 创建会议
|
||
func (m *MeetingApi) CreateMeeting(c *gin.Context) {
|
||
var (
|
||
req apistruct.CreateMeetingReq
|
||
resp apistruct.CreateMeetingResp
|
||
)
|
||
if err := c.BindJSON(&req); err != nil {
|
||
apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())
|
||
return
|
||
}
|
||
|
||
// 获取当前用户ID
|
||
creatorUserID := mcontext.GetOpUserID(c)
|
||
if creatorUserID == "" {
|
||
apiresp.GinError(c, errs.ErrArgs.WrapMsg("user id not found"))
|
||
return
|
||
}
|
||
|
||
// 生成会议ID
|
||
meetingID, err := m.genMeetingID(c, req.MeetingID)
|
||
if err != nil {
|
||
log.ZError(c, "CreateMeeting: failed to generate meeting id", err)
|
||
apiresp.GinError(c, err)
|
||
return
|
||
}
|
||
|
||
// 验证预约时间不能早于当前时间
|
||
scheduledTime := time.UnixMilli(req.ScheduledTime)
|
||
if scheduledTime.Before(time.Now()) {
|
||
apiresp.GinError(c, errs.ErrArgs.WrapMsg("scheduled time cannot be earlier than current time"))
|
||
return
|
||
}
|
||
|
||
// 处理密码:如果未提供,自动生成6位数字密码
|
||
password := req.Password
|
||
if password == "" {
|
||
password = m.genMeetingPassword()
|
||
} else {
|
||
// 验证密码格式:必须是6位数字
|
||
if len(password) != 6 {
|
||
apiresp.GinError(c, errs.ErrArgs.WrapMsg("password must be 6 digits"))
|
||
return
|
||
}
|
||
for _, char := range password {
|
||
if char < '0' || char > '9' {
|
||
apiresp.GinError(c, errs.ErrArgs.WrapMsg("password must be 6 digits"))
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
// 创建群聊,群聊名称为"会议群-[会议主题]"
|
||
groupName := fmt.Sprintf("会议群-%s", req.Subject)
|
||
// 在Ex字段中设置会议标识
|
||
exData := map[string]interface{}{
|
||
"isMeetingGroup": true,
|
||
"meetingID": meetingID,
|
||
}
|
||
exJSON, err := json.Marshal(exData)
|
||
if err != nil {
|
||
log.ZError(c, "CreateMeeting: failed to marshal ex data", err)
|
||
apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to marshal ex data"))
|
||
return
|
||
}
|
||
|
||
groupInfo := &sdkws.GroupInfo{
|
||
GroupName: groupName,
|
||
GroupType: constant.WorkingGroup,
|
||
FaceURL: req.CoverURL, // 使用会议封面作为群头像
|
||
Ex: string(exJSON), // 设置会议标识
|
||
NeedVerification: constant.Directly, // 设置为直接加入,任何人都能随意加群
|
||
}
|
||
|
||
// 设置群主和管理员:第一个主播为群主,其他主播为管理员
|
||
var ownerUserID string
|
||
var adminUserIDs []string
|
||
var memberUserIDs []string
|
||
if len(req.AnchorUserIDs) > 0 {
|
||
// 有主播时,第一个主播为群主,其他主播为管理员
|
||
ownerUserID = req.AnchorUserIDs[0]
|
||
if len(req.AnchorUserIDs) > 1 {
|
||
adminUserIDs = req.AnchorUserIDs[1:]
|
||
}
|
||
// 如果创建者不在主播列表中,将创建者作为普通成员加入
|
||
anchorMap := make(map[string]bool)
|
||
for _, anchorID := range req.AnchorUserIDs {
|
||
anchorMap[anchorID] = true
|
||
}
|
||
if !anchorMap[creatorUserID] {
|
||
memberUserIDs = []string{creatorUserID}
|
||
}
|
||
} else {
|
||
// 没有主播时,创建者为群主
|
||
ownerUserID = creatorUserID
|
||
}
|
||
|
||
createGroupReq := &group.CreateGroupReq{
|
||
OwnerUserID: ownerUserID,
|
||
AdminUserIDs: adminUserIDs,
|
||
MemberUserIDs: memberUserIDs,
|
||
GroupInfo: groupInfo,
|
||
}
|
||
|
||
createGroupResp, err := m.groupClient.GroupClient.CreateGroup(c, createGroupReq)
|
||
if err != nil {
|
||
log.ZError(c, "CreateMeeting: failed to create group", err, "meetingID", meetingID)
|
||
apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to create group"))
|
||
return
|
||
}
|
||
|
||
groupID := createGroupResp.GroupInfo.GroupID
|
||
|
||
// 兜底:再次对主播列表进行角色同步,确保主播=群主/管理员
|
||
if len(req.AnchorUserIDs) > 0 {
|
||
if err := m.updateGroupAnchors(c, groupID, req.AnchorUserIDs); err != nil {
|
||
log.ZWarn(c, "CreateMeeting: failed to sync anchors to owner/admin", err, "groupID", groupID, "anchorUserIDs", req.AnchorUserIDs)
|
||
}
|
||
}
|
||
|
||
// 收集所有群成员ID(群主、管理员、普通成员),用于创建会话
|
||
allMemberUserIDs := make([]string, 0)
|
||
allMemberUserIDs = append(allMemberUserIDs, ownerUserID)
|
||
allMemberUserIDs = append(allMemberUserIDs, adminUserIDs...)
|
||
allMemberUserIDs = append(allMemberUserIDs, memberUserIDs...)
|
||
// 去重
|
||
allMemberUserIDs = datautil.Distinct(allMemberUserIDs)
|
||
|
||
// 为所有群成员创建会话记录
|
||
if len(allMemberUserIDs) > 0 {
|
||
if err := m.conversationClient.CreateGroupChatConversations(c, groupID, allMemberUserIDs); err != nil {
|
||
log.ZWarn(c, "CreateMeeting: failed to create group chat conversations", err, "groupID", groupID, "userIDs", allMemberUserIDs)
|
||
// 不阻断流程,继续创建会议,只是会话可能不会立即显示
|
||
}
|
||
}
|
||
|
||
// 根据评论开关设置群的禁言状态
|
||
// 如果开启评论,取消群禁言;如果关闭评论,禁言群
|
||
if req.EnableComment {
|
||
// 开启评论,取消群禁言
|
||
_, err := m.groupClient.GroupClient.CancelMuteGroup(c, &group.CancelMuteGroupReq{
|
||
GroupID: groupID,
|
||
})
|
||
if err != nil {
|
||
log.ZWarn(c, "CreateMeeting: failed to cancel mute group", err, "groupID", groupID)
|
||
// 不阻断流程,继续创建会议
|
||
}
|
||
} else {
|
||
// 关闭评论,禁言群
|
||
_, err := m.groupClient.GroupClient.MuteGroup(c, &group.MuteGroupReq{
|
||
GroupID: groupID,
|
||
})
|
||
if err != nil {
|
||
log.ZWarn(c, "CreateMeeting: failed to mute group", err, "groupID", groupID)
|
||
// 不阻断流程,继续创建会议
|
||
}
|
||
}
|
||
|
||
// 创建会议记录
|
||
meeting := &model.Meeting{
|
||
MeetingID: meetingID,
|
||
Subject: req.Subject,
|
||
CoverURL: req.CoverURL,
|
||
ScheduledTime: scheduledTime,
|
||
Status: model.MeetingStatusScheduled,
|
||
CreatorUserID: creatorUserID,
|
||
Description: req.Description,
|
||
Duration: req.Duration,
|
||
EstimatedCount: req.EstimatedCount,
|
||
EnableMic: req.EnableMic,
|
||
EnableComment: req.EnableComment,
|
||
AnchorUserIDs: req.AnchorUserIDs,
|
||
GroupID: groupID,
|
||
CheckInCount: 0, // 初始化签到统计为0
|
||
Password: password, // 会议密码
|
||
}
|
||
|
||
if err := m.meetingDB.Create(c, meeting); err != nil {
|
||
log.ZError(c, "CreateMeeting: failed to create meeting", err, "meetingID", meetingID)
|
||
// 如果创建会议失败,尝试删除已创建的群聊,避免孤立群
|
||
if groupID != "" {
|
||
if dismissErr := m.groupClient.DismissGroup(c, groupID, true); dismissErr != nil {
|
||
log.ZWarn(c, "CreateMeeting: failed to rollback created group", dismissErr, "meetingID", meetingID, "groupID", groupID)
|
||
}
|
||
}
|
||
apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to create meeting"))
|
||
return
|
||
}
|
||
|
||
// 加载主播信息
|
||
anchorUsers := m.loadAnchorUsers(c, meeting.AnchorUserIDs)
|
||
|
||
// 转换为响应格式
|
||
resp.MeetingInfo = &apistruct.MeetingInfo{
|
||
MeetingID: meeting.MeetingID,
|
||
Subject: meeting.Subject,
|
||
CoverURL: meeting.CoverURL,
|
||
ScheduledTime: meeting.ScheduledTime.UnixMilli(),
|
||
Status: meeting.Status,
|
||
CreatorUserID: meeting.CreatorUserID,
|
||
Description: meeting.Description,
|
||
Duration: meeting.Duration,
|
||
EstimatedCount: meeting.EstimatedCount,
|
||
EnableMic: meeting.EnableMic,
|
||
EnableComment: meeting.EnableComment,
|
||
AnchorUserIDs: meeting.AnchorUserIDs,
|
||
AnchorUsers: anchorUsers,
|
||
CreateTime: meeting.CreateTime.UnixMilli(),
|
||
UpdateTime: meeting.UpdateTime.UnixMilli(),
|
||
Ex: meeting.Ex,
|
||
GroupID: meeting.GroupID,
|
||
CheckInCount: meeting.CheckInCount,
|
||
Password: meeting.Password,
|
||
}
|
||
resp.GroupID = groupID
|
||
|
||
// 将会议群ID添加到webhook配置的attentionIds中
|
||
if m.systemConfigDB != nil && groupID != "" {
|
||
if err := webhook.UpdateAttentionIds(c, m.systemConfigDB, groupID, true); err != nil {
|
||
log.ZWarn(c, "CreateMeeting: failed to add groupID to webhook attentionIds", err, "meetingID", meetingID, "groupID", groupID)
|
||
}
|
||
}
|
||
|
||
log.ZInfo(c, "CreateMeeting: success", "meetingID", meetingID, "groupID", groupID, "creatorUserID", creatorUserID)
|
||
apiresp.GinSuccess(c, resp)
|
||
}
|
||
|
||
// UpdateMeeting 更新会议(可以单独编辑某个字段)
|
||
func (m *MeetingApi) UpdateMeeting(c *gin.Context) {
|
||
var (
|
||
req apistruct.UpdateMeetingReq
|
||
resp apistruct.UpdateMeetingResp
|
||
)
|
||
if err := c.BindJSON(&req); err != nil {
|
||
apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())
|
||
return
|
||
}
|
||
|
||
// 获取当前用户ID
|
||
opUserID := mcontext.GetOpUserID(c)
|
||
|
||
// 查询会议是否存在
|
||
meeting, err := m.meetingDB.Take(c, req.MeetingID)
|
||
if err != nil {
|
||
if errs.ErrRecordNotFound.Is(err) {
|
||
apiresp.GinError(c, errs.ErrArgs.WrapMsg("meeting not found"))
|
||
return
|
||
}
|
||
log.ZError(c, "UpdateMeeting: failed to get meeting", err, "meetingID", req.MeetingID)
|
||
apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to get meeting"))
|
||
return
|
||
}
|
||
|
||
// 验证权限(只有创建者可以修改)
|
||
if meeting.CreatorUserID != opUserID {
|
||
apiresp.GinError(c, errs.ErrNoPermission.WrapMsg("only creator can update meeting"))
|
||
return
|
||
}
|
||
|
||
// 已结束或已取消的会议不允许修改
|
||
if meeting.Status == model.MeetingStatusFinished || meeting.Status == model.MeetingStatusCancelled {
|
||
apiresp.GinError(c, errs.ErrArgs.WrapMsg("cannot update finished or cancelled meeting"))
|
||
return
|
||
}
|
||
|
||
// 构建更新数据(只更新提供的字段)
|
||
updateData := make(map[string]any)
|
||
if req.Subject != "" {
|
||
updateData["subject"] = req.Subject
|
||
// 如果更新了主题,同步更新群聊名称
|
||
if meeting.GroupID != "" {
|
||
groupName := fmt.Sprintf("会议群-%s", req.Subject)
|
||
_, err := m.groupClient.GroupClient.SetGroupInfo(c, &group.SetGroupInfoReq{
|
||
GroupInfoForSet: &sdkws.GroupInfoForSet{
|
||
GroupID: meeting.GroupID,
|
||
GroupName: groupName,
|
||
},
|
||
})
|
||
if err != nil {
|
||
log.ZWarn(c, "UpdateMeeting: failed to update group name", err, "groupID", meeting.GroupID)
|
||
}
|
||
}
|
||
}
|
||
if req.CoverURL != "" {
|
||
updateData["cover_url"] = req.CoverURL
|
||
// 如果更新了封面,同步更新群聊头像
|
||
if meeting.GroupID != "" {
|
||
_, err := m.groupClient.GroupClient.SetGroupInfo(c, &group.SetGroupInfoReq{
|
||
GroupInfoForSet: &sdkws.GroupInfoForSet{
|
||
GroupID: meeting.GroupID,
|
||
FaceURL: req.CoverURL,
|
||
},
|
||
})
|
||
if err != nil {
|
||
log.ZWarn(c, "UpdateMeeting: failed to update group face", err, "groupID", meeting.GroupID)
|
||
}
|
||
}
|
||
}
|
||
if req.ScheduledTime > 0 {
|
||
scheduledTime := time.UnixMilli(req.ScheduledTime)
|
||
// 验证预约时间不能早于当前时间
|
||
if scheduledTime.Before(time.Now()) {
|
||
apiresp.GinError(c, errs.ErrArgs.WrapMsg("scheduled time cannot be earlier than current time"))
|
||
return
|
||
}
|
||
updateData["scheduled_time"] = scheduledTime
|
||
}
|
||
if req.Status > 0 {
|
||
// 验证状态值
|
||
if req.Status < 1 || req.Status > 4 {
|
||
apiresp.GinError(c, errs.ErrArgs.WrapMsg("invalid status, must be 1-4"))
|
||
return
|
||
}
|
||
updateData["status"] = req.Status
|
||
}
|
||
if req.Description != "" {
|
||
updateData["description"] = req.Description
|
||
}
|
||
if req.Duration > 0 {
|
||
updateData["duration"] = req.Duration
|
||
}
|
||
if req.EstimatedCount > 0 {
|
||
updateData["estimated_count"] = req.EstimatedCount
|
||
}
|
||
if req.EnableMic != nil {
|
||
updateData["enable_mic"] = *req.EnableMic
|
||
}
|
||
if req.EnableComment != nil {
|
||
updateData["enable_comment"] = *req.EnableComment
|
||
// 如果更新了评论开关,同步更新群的禁言状态
|
||
if meeting.GroupID != "" {
|
||
if *req.EnableComment {
|
||
// 开启评论,取消群禁言
|
||
_, err := m.groupClient.GroupClient.CancelMuteGroup(c, &group.CancelMuteGroupReq{
|
||
GroupID: meeting.GroupID,
|
||
})
|
||
if err != nil {
|
||
log.ZWarn(c, "UpdateMeeting: failed to cancel mute group", err, "groupID", meeting.GroupID)
|
||
// 不阻断流程,继续更新会议
|
||
}
|
||
} else {
|
||
// 关闭评论,禁言群
|
||
_, err := m.groupClient.GroupClient.MuteGroup(c, &group.MuteGroupReq{
|
||
GroupID: meeting.GroupID,
|
||
})
|
||
if err != nil {
|
||
log.ZWarn(c, "UpdateMeeting: failed to mute group", err, "groupID", meeting.GroupID)
|
||
// 不阻断流程,继续更新会议
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if req.AnchorUserIDs != nil {
|
||
updateData["anchor_user_ids"] = req.AnchorUserIDs
|
||
// 如果更新了主播列表,需要同步更新群的管理员和群主
|
||
if meeting.GroupID != "" {
|
||
if err := m.updateGroupAnchors(c, meeting.GroupID, req.AnchorUserIDs); err != nil {
|
||
log.ZWarn(c, "UpdateMeeting: failed to update group anchors", err, "groupID", meeting.GroupID, "anchorUserIDs", req.AnchorUserIDs)
|
||
// 不阻断流程,继续更新会议
|
||
}
|
||
}
|
||
}
|
||
if req.Password != nil {
|
||
password := *req.Password
|
||
// 验证密码格式:必须是6位数字
|
||
if password != "" {
|
||
if len(password) != 6 {
|
||
apiresp.GinError(c, errs.ErrArgs.WrapMsg("password must be 6 digits"))
|
||
return
|
||
}
|
||
for _, char := range password {
|
||
if char < '0' || char > '9' {
|
||
apiresp.GinError(c, errs.ErrArgs.WrapMsg("password must be 6 digits"))
|
||
return
|
||
}
|
||
}
|
||
}
|
||
updateData["password"] = password
|
||
}
|
||
if req.Ex != "" {
|
||
updateData["ex"] = req.Ex
|
||
}
|
||
|
||
if len(updateData) == 0 {
|
||
apiresp.GinError(c, errs.ErrArgs.WrapMsg("no fields to update"))
|
||
return
|
||
}
|
||
|
||
// 更新会议
|
||
if err := m.meetingDB.Update(c, req.MeetingID, updateData); err != nil {
|
||
log.ZError(c, "UpdateMeeting: failed to update meeting", err, "meetingID", req.MeetingID)
|
||
apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to update meeting"))
|
||
return
|
||
}
|
||
|
||
// 重新查询会议信息
|
||
meeting, err = m.meetingDB.Take(c, req.MeetingID)
|
||
if err != nil {
|
||
log.ZError(c, "UpdateMeeting: failed to get updated meeting", err, "meetingID", req.MeetingID)
|
||
apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to get updated meeting"))
|
||
return
|
||
}
|
||
|
||
// 加载主播信息
|
||
anchorUsers := m.loadAnchorUsers(c, meeting.AnchorUserIDs)
|
||
|
||
// 转换为响应格式
|
||
resp.MeetingInfo = &apistruct.MeetingInfo{
|
||
MeetingID: meeting.MeetingID,
|
||
Subject: meeting.Subject,
|
||
CoverURL: meeting.CoverURL,
|
||
ScheduledTime: meeting.ScheduledTime.UnixMilli(),
|
||
Status: meeting.Status,
|
||
CreatorUserID: meeting.CreatorUserID,
|
||
Description: meeting.Description,
|
||
Duration: meeting.Duration,
|
||
EstimatedCount: meeting.EstimatedCount,
|
||
EnableMic: meeting.EnableMic,
|
||
EnableComment: meeting.EnableComment,
|
||
AnchorUserIDs: meeting.AnchorUserIDs,
|
||
AnchorUsers: anchorUsers,
|
||
CreateTime: meeting.CreateTime.UnixMilli(),
|
||
UpdateTime: meeting.UpdateTime.UnixMilli(),
|
||
Ex: meeting.Ex,
|
||
GroupID: meeting.GroupID,
|
||
CheckInCount: meeting.CheckInCount,
|
||
Password: meeting.Password,
|
||
}
|
||
|
||
log.ZInfo(c, "UpdateMeeting: success", "meetingID", req.MeetingID)
|
||
apiresp.GinSuccess(c, resp)
|
||
}
|
||
|
||
// updateGroupAnchors 更新群的主播(群主和管理员)
|
||
func (m *MeetingApi) updateGroupAnchors(ctx context.Context, groupID string, newAnchorUserIDs []string) error {
|
||
// 获取当前群的所有成员
|
||
currentMemberUserIDs, err := m.groupClient.GetGroupMemberUserIDs(ctx, groupID)
|
||
if err != nil {
|
||
return errs.WrapMsg(err, "failed to get group members")
|
||
}
|
||
|
||
// 构建当前成员的映射
|
||
currentMemberMap := make(map[string]bool)
|
||
for _, userID := range currentMemberUserIDs {
|
||
currentMemberMap[userID] = true
|
||
}
|
||
|
||
// 找出不在群里的主播,先把他们拉进群
|
||
var usersToInvite []string
|
||
for _, anchorID := range newAnchorUserIDs {
|
||
if !currentMemberMap[anchorID] {
|
||
usersToInvite = append(usersToInvite, anchorID)
|
||
}
|
||
}
|
||
|
||
// 邀请不在群里的主播进群
|
||
if len(usersToInvite) > 0 {
|
||
_, err := m.groupClient.GroupClient.InviteUserToGroup(ctx, &group.InviteUserToGroupReq{
|
||
GroupID: groupID,
|
||
InvitedUserIDs: usersToInvite,
|
||
})
|
||
if err != nil {
|
||
return errs.WrapMsg(err, "failed to invite anchors to group")
|
||
}
|
||
|
||
// 为新加入的成员创建会话记录
|
||
if err := m.conversationClient.CreateGroupChatConversations(ctx, groupID, usersToInvite); err != nil {
|
||
log.ZWarn(ctx, "updateGroupAnchors: failed to create conversations for new members", err, "groupID", groupID, "userIDs", usersToInvite)
|
||
// 不阻断流程,继续执行
|
||
}
|
||
}
|
||
|
||
// 如果没有主播,不需要更新群主和管理员
|
||
if len(newAnchorUserIDs) == 0 {
|
||
return nil
|
||
}
|
||
|
||
// 获取当前群主
|
||
groupInfo, err := m.groupClient.GetGroupInfo(ctx, groupID)
|
||
if err != nil {
|
||
return errs.WrapMsg(err, "failed to get group info")
|
||
}
|
||
|
||
currentOwnerUserID := groupInfo.OwnerUserID
|
||
newOwnerUserID := newAnchorUserIDs[0] // 第一个主播为群主
|
||
var newAdminUserIDs []string
|
||
if len(newAnchorUserIDs) > 1 {
|
||
newAdminUserIDs = newAnchorUserIDs[1:] // 其他主播为管理员
|
||
}
|
||
|
||
// 获取所有群成员信息(包括角色)
|
||
allMemberUserIDs := append([]string{currentOwnerUserID}, newAnchorUserIDs...)
|
||
allMemberUserIDs = datautil.Distinct(allMemberUserIDs)
|
||
members, err := m.groupClient.GetGroupMembersInfo(ctx, groupID, allMemberUserIDs)
|
||
if err != nil {
|
||
return errs.WrapMsg(err, "failed to get group members info")
|
||
}
|
||
|
||
// 构建成员映射
|
||
memberMap := make(map[string]*sdkws.GroupMemberFullInfo)
|
||
for _, member := range members {
|
||
memberMap[member.UserID] = member
|
||
}
|
||
|
||
// 如果群主需要变更,先转移群主
|
||
if currentOwnerUserID != newOwnerUserID {
|
||
// 确保新群主在群里
|
||
if _, exists := memberMap[newOwnerUserID]; !exists {
|
||
return errs.ErrArgs.WrapMsg("new owner not in group")
|
||
}
|
||
|
||
// 获取当前群主的角色级别(用于转移后设置)
|
||
currentOwnerMember := memberMap[currentOwnerUserID]
|
||
var oldOwnerNewRoleLevel int32 = constant.GroupOrdinaryUsers
|
||
if currentOwnerMember != nil {
|
||
oldOwnerNewRoleLevel = currentOwnerMember.RoleLevel
|
||
}
|
||
|
||
// 如果当前群主不在新主播列表中,降级为普通成员
|
||
anchorMap := make(map[string]bool)
|
||
for _, anchorID := range newAnchorUserIDs {
|
||
anchorMap[anchorID] = true
|
||
}
|
||
if !anchorMap[currentOwnerUserID] {
|
||
oldOwnerNewRoleLevel = constant.GroupOrdinaryUsers
|
||
} else {
|
||
// 如果当前群主在新主播列表中但不是第一个,降级为管理员
|
||
if currentOwnerUserID != newOwnerUserID {
|
||
oldOwnerNewRoleLevel = constant.GroupAdmin
|
||
}
|
||
}
|
||
|
||
// 转移群主
|
||
_, err := m.groupClient.GroupClient.TransferGroupOwner(ctx, &group.TransferGroupOwnerReq{
|
||
GroupID: groupID,
|
||
OldOwnerUserID: currentOwnerUserID,
|
||
NewOwnerUserID: newOwnerUserID,
|
||
})
|
||
if err != nil {
|
||
return errs.WrapMsg(err, "failed to transfer group owner")
|
||
}
|
||
|
||
// 如果旧群主需要降级,设置其角色
|
||
if oldOwnerNewRoleLevel != constant.GroupOwner {
|
||
_, err := m.groupClient.GroupClient.SetGroupMemberInfo(ctx, &group.SetGroupMemberInfoReq{
|
||
Members: []*group.SetGroupMemberInfo{
|
||
{
|
||
GroupID: groupID,
|
||
UserID: currentOwnerUserID,
|
||
RoleLevel: &wrapperspb.Int32Value{Value: oldOwnerNewRoleLevel},
|
||
},
|
||
},
|
||
})
|
||
if err != nil {
|
||
log.ZWarn(ctx, "updateGroupAnchors: failed to set old owner role", err, "groupID", groupID, "userID", currentOwnerUserID, "roleLevel", oldOwnerNewRoleLevel)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 设置其他主播为管理员
|
||
if len(newAdminUserIDs) > 0 {
|
||
var membersToUpdate []*group.SetGroupMemberInfo
|
||
for _, adminID := range newAdminUserIDs {
|
||
// 检查该成员当前角色
|
||
member := memberMap[adminID]
|
||
if member == nil || member.RoleLevel != constant.GroupAdmin {
|
||
membersToUpdate = append(membersToUpdate, &group.SetGroupMemberInfo{
|
||
GroupID: groupID,
|
||
UserID: adminID,
|
||
RoleLevel: &wrapperspb.Int32Value{Value: constant.GroupAdmin},
|
||
})
|
||
}
|
||
}
|
||
|
||
if len(membersToUpdate) > 0 {
|
||
_, err := m.groupClient.GroupClient.SetGroupMemberInfo(ctx, &group.SetGroupMemberInfoReq{
|
||
Members: membersToUpdate,
|
||
})
|
||
if err != nil {
|
||
log.ZWarn(ctx, "updateGroupAnchors: failed to set admins", err, "groupID", groupID, "adminUserIDs", newAdminUserIDs)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果原来的管理员不在新主播列表中,降级为普通成员
|
||
// 获取所有当前管理员
|
||
var currentAdminsToDemote []string
|
||
for userID, member := range memberMap {
|
||
if member.RoleLevel == constant.GroupAdmin {
|
||
// 检查是否在新主播列表中
|
||
isInNewAnchors := false
|
||
for _, anchorID := range newAnchorUserIDs {
|
||
if anchorID == userID {
|
||
isInNewAnchors = true
|
||
break
|
||
}
|
||
}
|
||
// 如果不在新主播列表中,需要降级
|
||
if !isInNewAnchors {
|
||
currentAdminsToDemote = append(currentAdminsToDemote, userID)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 降级不在新主播列表中的管理员
|
||
if len(currentAdminsToDemote) > 0 {
|
||
var membersToDemote []*group.SetGroupMemberInfo
|
||
for _, userID := range currentAdminsToDemote {
|
||
membersToDemote = append(membersToDemote, &group.SetGroupMemberInfo{
|
||
GroupID: groupID,
|
||
UserID: userID,
|
||
RoleLevel: &wrapperspb.Int32Value{Value: constant.GroupOrdinaryUsers},
|
||
})
|
||
}
|
||
|
||
_, err := m.groupClient.GroupClient.SetGroupMemberInfo(ctx, &group.SetGroupMemberInfoReq{
|
||
Members: membersToDemote,
|
||
})
|
||
if err != nil {
|
||
log.ZWarn(ctx, "updateGroupAnchors: failed to demote admins", err, "groupID", groupID, "userIDs", currentAdminsToDemote)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetMeetings 获取会议列表
|
||
func (m *MeetingApi) GetMeetings(c *gin.Context) {
|
||
var (
|
||
req apistruct.GetMeetingsReq
|
||
resp apistruct.GetMeetingsResp
|
||
)
|
||
if err := c.BindJSON(&req); err != nil {
|
||
apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())
|
||
return
|
||
}
|
||
|
||
// 设置默认分页参数
|
||
if req.Pagination.PageNumber <= 0 {
|
||
req.Pagination.PageNumber = 1
|
||
}
|
||
if req.Pagination.ShowNumber <= 0 {
|
||
req.Pagination.ShowNumber = 20
|
||
}
|
||
|
||
// 创建分页对象
|
||
pagination := &meetingPaginationWrapper{
|
||
pageNumber: req.Pagination.PageNumber,
|
||
showNumber: req.Pagination.ShowNumber,
|
||
}
|
||
|
||
var total int64
|
||
var meetings []*model.Meeting
|
||
var err error
|
||
|
||
// 根据查询条件查询会议
|
||
if req.CreatorUserID != "" {
|
||
// 按创建者查询
|
||
total, meetings, err = m.meetingDB.FindByCreator(c, req.CreatorUserID, pagination)
|
||
} else if req.Status > 0 {
|
||
// 按状态查询
|
||
total, meetings, err = m.meetingDB.FindByStatus(c, req.Status, pagination)
|
||
} else if req.Keyword != "" {
|
||
// 按关键词搜索
|
||
total, meetings, err = m.meetingDB.Search(c, req.Keyword, pagination)
|
||
} else if req.StartTime > 0 && req.EndTime > 0 {
|
||
// 按时间范围查询
|
||
total, meetings, err = m.meetingDB.FindByScheduledTimeRange(c, req.StartTime, req.EndTime, pagination)
|
||
} else {
|
||
// 查询所有会议
|
||
total, meetings, err = m.meetingDB.FindAll(c, pagination)
|
||
}
|
||
|
||
if err != nil {
|
||
log.ZError(c, "GetMeetings: failed to find meetings", err)
|
||
apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to find meetings"))
|
||
return
|
||
}
|
||
|
||
// 收集所有主播ID
|
||
allAnchorUserIDs := make([]string, 0)
|
||
for _, meeting := range meetings {
|
||
allAnchorUserIDs = append(allAnchorUserIDs, meeting.AnchorUserIDs...)
|
||
}
|
||
// 去重
|
||
allAnchorUserIDs = datautil.Distinct(allAnchorUserIDs)
|
||
|
||
// 批量获取主播用户信息
|
||
anchorUserMap := make(map[string]*sdkws.UserInfo)
|
||
if len(allAnchorUserIDs) > 0 {
|
||
anchorUsers, err := m.userClient.GetUsersInfo(c, allAnchorUserIDs)
|
||
if err != nil {
|
||
log.ZWarn(c, "GetMeetings: failed to get anchor users info", err, "anchorUserIDs", allAnchorUserIDs)
|
||
// 不阻断流程,继续返回会议列表,只是没有主播信息
|
||
} else {
|
||
for _, user := range anchorUsers {
|
||
anchorUserMap[user.UserID] = user
|
||
}
|
||
}
|
||
}
|
||
|
||
// 转换为响应格式
|
||
meetingInfos := make([]*apistruct.MeetingInfo, 0, len(meetings))
|
||
for _, meeting := range meetings {
|
||
// 获取该会议的主播信息
|
||
anchorUsers := make([]*sdkws.UserInfo, 0, len(meeting.AnchorUserIDs))
|
||
for _, anchorID := range meeting.AnchorUserIDs {
|
||
if user, ok := anchorUserMap[anchorID]; ok {
|
||
anchorUsers = append(anchorUsers, user)
|
||
}
|
||
}
|
||
|
||
meetingInfos = append(meetingInfos, &apistruct.MeetingInfo{
|
||
MeetingID: meeting.MeetingID,
|
||
Subject: meeting.Subject,
|
||
CoverURL: meeting.CoverURL,
|
||
ScheduledTime: meeting.ScheduledTime.UnixMilli(),
|
||
Status: meeting.Status,
|
||
CreatorUserID: meeting.CreatorUserID,
|
||
Description: meeting.Description,
|
||
Duration: meeting.Duration,
|
||
EstimatedCount: meeting.EstimatedCount,
|
||
EnableMic: meeting.EnableMic,
|
||
EnableComment: meeting.EnableComment,
|
||
AnchorUserIDs: meeting.AnchorUserIDs,
|
||
AnchorUsers: anchorUsers,
|
||
CreateTime: meeting.CreateTime.UnixMilli(),
|
||
UpdateTime: meeting.UpdateTime.UnixMilli(),
|
||
Ex: meeting.Ex,
|
||
GroupID: meeting.GroupID,
|
||
CheckInCount: meeting.CheckInCount,
|
||
Password: meeting.Password,
|
||
})
|
||
}
|
||
|
||
resp.Total = total
|
||
resp.Meetings = meetingInfos
|
||
|
||
log.ZInfo(c, "GetMeetings: success", "total", total, "count", len(meetingInfos))
|
||
apiresp.GinSuccess(c, resp)
|
||
}
|
||
|
||
// DeleteMeeting 删除会议
|
||
func (m *MeetingApi) DeleteMeeting(c *gin.Context) {
|
||
var (
|
||
req apistruct.DeleteMeetingReq
|
||
resp apistruct.DeleteMeetingResp
|
||
)
|
||
if err := c.BindJSON(&req); err != nil {
|
||
apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())
|
||
return
|
||
}
|
||
|
||
// 获取当前用户ID
|
||
opUserID := mcontext.GetOpUserID(c)
|
||
|
||
// 查询会议是否存在
|
||
meeting, err := m.meetingDB.Take(c, req.MeetingID)
|
||
if err != nil {
|
||
if errs.ErrRecordNotFound.Is(err) {
|
||
apiresp.GinError(c, errs.ErrArgs.WrapMsg("meeting not found"))
|
||
return
|
||
}
|
||
log.ZError(c, "DeleteMeeting: failed to get meeting", err, "meetingID", req.MeetingID)
|
||
apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to get meeting"))
|
||
return
|
||
}
|
||
|
||
// 验证权限(只有创建者可以删除)
|
||
if meeting.CreatorUserID != opUserID {
|
||
apiresp.GinError(c, errs.ErrNoPermission.WrapMsg("only creator can delete meeting"))
|
||
return
|
||
}
|
||
|
||
// 如果有关联的群聊,先解散群聊
|
||
if meeting.GroupID != "" {
|
||
err := m.groupClient.DismissGroup(c, meeting.GroupID, false)
|
||
if err != nil {
|
||
log.ZWarn(c, "DeleteMeeting: failed to dismiss group", err, "meetingID", req.MeetingID, "groupID", meeting.GroupID)
|
||
// 不阻断流程,继续删除会议
|
||
} else {
|
||
log.ZInfo(c, "DeleteMeeting: successfully dismissed group", "meetingID", req.MeetingID, "groupID", meeting.GroupID)
|
||
}
|
||
|
||
// 从webhook配置的attentionIds中移除会议群ID
|
||
if m.systemConfigDB != nil {
|
||
if err := webhook.UpdateAttentionIds(c, m.systemConfigDB, meeting.GroupID, false); err != nil {
|
||
log.ZWarn(c, "DeleteMeeting: failed to remove groupID from webhook attentionIds", err, "meetingID", req.MeetingID, "groupID", meeting.GroupID)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 删除会议
|
||
if err := m.meetingDB.Delete(c, req.MeetingID); err != nil {
|
||
log.ZError(c, "DeleteMeeting: failed to delete meeting", err, "meetingID", req.MeetingID)
|
||
apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to delete meeting"))
|
||
return
|
||
}
|
||
|
||
log.ZInfo(c, "DeleteMeeting: success", "meetingID", req.MeetingID)
|
||
apiresp.GinSuccess(c, resp)
|
||
}
|
||
|
||
// GetMeetingPublic 获取会议信息(用户端)
|
||
func (m *MeetingApi) GetMeetingPublic(c *gin.Context) {
|
||
var (
|
||
req apistruct.GetMeetingReq
|
||
resp apistruct.GetMeetingResp
|
||
)
|
||
if err := c.BindJSON(&req); err != nil {
|
||
apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())
|
||
return
|
||
}
|
||
|
||
// 查询会议
|
||
meeting, err := m.meetingDB.Take(c, req.MeetingID)
|
||
if err != nil {
|
||
if m.IsNotFound(err) {
|
||
apiresp.GinError(c, errs.ErrArgs.WrapMsg("meeting not found"))
|
||
return
|
||
}
|
||
log.ZError(c, "GetMeetingPublic: failed to get meeting", err, "meetingID", req.MeetingID)
|
||
apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to get meeting"))
|
||
return
|
||
}
|
||
|
||
// 用户端只能查看已预约(1)和进行中(2)的会议
|
||
if meeting.Status != model.MeetingStatusScheduled && meeting.Status != model.MeetingStatusOngoing {
|
||
apiresp.GinError(c, errs.ErrArgs.WrapMsg("meeting not available"))
|
||
return
|
||
}
|
||
|
||
// 加载主播信息
|
||
anchorUsers := m.loadAnchorUsers(c, meeting.AnchorUserIDs)
|
||
|
||
// 转换为用户端响应格式(过滤管理字段)
|
||
resp.MeetingInfo = &apistruct.MeetingPublicInfo{
|
||
MeetingID: meeting.MeetingID,
|
||
Subject: meeting.Subject,
|
||
CoverURL: meeting.CoverURL,
|
||
ScheduledTime: meeting.ScheduledTime.UnixMilli(),
|
||
Status: meeting.Status,
|
||
Description: meeting.Description,
|
||
Duration: meeting.Duration,
|
||
EnableMic: meeting.EnableMic,
|
||
EnableComment: meeting.EnableComment,
|
||
AnchorUsers: anchorUsers,
|
||
GroupID: meeting.GroupID,
|
||
CheckInCount: meeting.CheckInCount,
|
||
Password: meeting.Password,
|
||
}
|
||
|
||
apiresp.GinSuccess(c, resp)
|
||
}
|
||
|
||
// GetMeetingsPublic 获取会议列表(用户端)
|
||
func (m *MeetingApi) GetMeetingsPublic(c *gin.Context) {
|
||
var (
|
||
req apistruct.GetMeetingsPublicReq
|
||
resp apistruct.GetMeetingsPublicResp
|
||
)
|
||
if err := c.BindJSON(&req); err != nil {
|
||
apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())
|
||
return
|
||
}
|
||
|
||
// 设置默认分页参数
|
||
if req.Pagination.PageNumber <= 0 {
|
||
req.Pagination.PageNumber = 1
|
||
}
|
||
if req.Pagination.ShowNumber <= 0 {
|
||
req.Pagination.ShowNumber = 20
|
||
}
|
||
|
||
// 创建分页对象
|
||
pagination := &meetingPaginationWrapper{
|
||
pageNumber: req.Pagination.PageNumber,
|
||
showNumber: req.Pagination.ShowNumber,
|
||
}
|
||
|
||
var total int64
|
||
var meetings []*model.Meeting
|
||
var err error
|
||
|
||
// 用户端只显示已预约(1)和进行中(2)的会议
|
||
// 如果指定了状态,验证是否为允许的状态
|
||
if req.Status > 0 {
|
||
if req.Status != model.MeetingStatusScheduled && req.Status != model.MeetingStatusOngoing {
|
||
apiresp.GinError(c, errs.ErrArgs.WrapMsg("user can only view scheduled (1) or ongoing (2) meetings"))
|
||
return
|
||
}
|
||
// 按状态查询
|
||
total, meetings, err = m.meetingDB.FindByStatus(c, req.Status, pagination)
|
||
} else {
|
||
// 对于其他查询方式,先查询再过滤状态
|
||
var allMeetings []*model.Meeting
|
||
if req.Keyword != "" {
|
||
// 按关键词搜索
|
||
total, allMeetings, err = m.meetingDB.Search(c, req.Keyword, pagination)
|
||
} else if req.StartTime > 0 && req.EndTime > 0 {
|
||
// 按时间范围查询
|
||
total, allMeetings, err = m.meetingDB.FindByScheduledTimeRange(c, req.StartTime, req.EndTime, pagination)
|
||
} else {
|
||
// 查询所有会议
|
||
total, allMeetings, err = m.meetingDB.FindAll(c, pagination)
|
||
}
|
||
|
||
if err == nil {
|
||
// 过滤只保留已预约和进行中的会议
|
||
meetings = make([]*model.Meeting, 0, len(allMeetings))
|
||
for _, meeting := range allMeetings {
|
||
if meeting.Status == model.MeetingStatusScheduled || meeting.Status == model.MeetingStatusOngoing {
|
||
meetings = append(meetings, meeting)
|
||
}
|
||
}
|
||
total = int64(len(meetings))
|
||
}
|
||
}
|
||
|
||
if err != nil {
|
||
log.ZError(c, "GetMeetingsPublic: failed to find meetings", err)
|
||
apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to find meetings"))
|
||
return
|
||
}
|
||
|
||
// 收集所有主播ID
|
||
allAnchorUserIDs := make([]string, 0)
|
||
for _, meeting := range meetings {
|
||
allAnchorUserIDs = append(allAnchorUserIDs, meeting.AnchorUserIDs...)
|
||
}
|
||
// 去重
|
||
allAnchorUserIDs = datautil.Distinct(allAnchorUserIDs)
|
||
|
||
// 批量获取主播用户信息
|
||
anchorUserMap := make(map[string]*sdkws.UserInfo)
|
||
if len(allAnchorUserIDs) > 0 {
|
||
anchorUsers, err := m.userClient.GetUsersInfo(c, allAnchorUserIDs)
|
||
if err != nil {
|
||
log.ZWarn(c, "GetMeetingsPublic: failed to get anchor users info", err, "anchorUserIDs", allAnchorUserIDs)
|
||
// 不阻断流程,继续返回会议列表,只是没有主播信息
|
||
} else {
|
||
for _, user := range anchorUsers {
|
||
anchorUserMap[user.UserID] = user
|
||
}
|
||
}
|
||
}
|
||
|
||
// 转换为用户端响应格式(过滤管理字段)
|
||
meetingInfos := make([]*apistruct.MeetingPublicInfo, 0, len(meetings))
|
||
for _, meeting := range meetings {
|
||
// 获取该会议的主播信息
|
||
anchorUsers := make([]*sdkws.UserInfo, 0, len(meeting.AnchorUserIDs))
|
||
for _, anchorID := range meeting.AnchorUserIDs {
|
||
if user, ok := anchorUserMap[anchorID]; ok {
|
||
anchorUsers = append(anchorUsers, user)
|
||
}
|
||
}
|
||
|
||
meetingInfos = append(meetingInfos, &apistruct.MeetingPublicInfo{
|
||
MeetingID: meeting.MeetingID,
|
||
Subject: meeting.Subject,
|
||
CoverURL: meeting.CoverURL,
|
||
ScheduledTime: meeting.ScheduledTime.UnixMilli(),
|
||
Status: meeting.Status,
|
||
Description: meeting.Description,
|
||
Duration: meeting.Duration,
|
||
EnableMic: meeting.EnableMic,
|
||
EnableComment: meeting.EnableComment,
|
||
AnchorUsers: anchorUsers,
|
||
GroupID: meeting.GroupID,
|
||
CheckInCount: meeting.CheckInCount,
|
||
Password: meeting.Password,
|
||
})
|
||
}
|
||
|
||
resp.Total = total
|
||
resp.Meetings = meetingInfos
|
||
|
||
log.ZInfo(c, "GetMeetingsPublic: success", "total", total, "count", len(meetingInfos))
|
||
apiresp.GinSuccess(c, resp)
|
||
}
|
||
|
||
// CheckInMeeting 会议签到
|
||
func (m *MeetingApi) CheckInMeeting(c *gin.Context) {
|
||
var (
|
||
req apistruct.CheckInMeetingReq
|
||
resp apistruct.CheckInMeetingResp
|
||
)
|
||
if err := c.BindJSON(&req); err != nil {
|
||
apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())
|
||
return
|
||
}
|
||
|
||
// 获取当前用户ID
|
||
userID := mcontext.GetOpUserID(c)
|
||
if userID == "" {
|
||
apiresp.GinError(c, errs.ErrArgs.WrapMsg("user id not found"))
|
||
return
|
||
}
|
||
|
||
// 验证会议是否存在
|
||
meeting, err := m.meetingDB.Take(c, req.MeetingID)
|
||
if err != nil {
|
||
if m.IsNotFound(err) {
|
||
apiresp.GinError(c, errs.ErrArgs.WrapMsg("meeting not found"))
|
||
return
|
||
}
|
||
log.ZError(c, "CheckInMeeting: failed to get meeting", err, "meetingID", req.MeetingID)
|
||
apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to get meeting"))
|
||
return
|
||
}
|
||
|
||
// 验证会议状态(只能对已预约和进行中的会议签到)
|
||
if meeting.Status != model.MeetingStatusScheduled && meeting.Status != model.MeetingStatusOngoing {
|
||
apiresp.GinError(c, errs.ErrArgs.WrapMsg("can only check in for scheduled or ongoing meetings"))
|
||
return
|
||
}
|
||
|
||
// 检查是否已经签到
|
||
existingCheckIn, err := m.meetingCheckInDB.FindByUserAndMeetingID(c, userID, req.MeetingID)
|
||
if err == nil && existingCheckIn != nil {
|
||
apiresp.GinError(c, errs.ErrArgs.WrapMsg("already checked in"))
|
||
return
|
||
}
|
||
if err != nil && !m.IsNotFound(err) {
|
||
log.ZError(c, "CheckInMeeting: failed to check existing check-in", err, "userID", userID, "meetingID", req.MeetingID)
|
||
apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to check existing check-in"))
|
||
return
|
||
}
|
||
|
||
// 生成签到ID
|
||
checkInID := idutil.GetMsgIDByMD5(userID + req.MeetingID + timeutil.GetCurrentTimeFormatted())
|
||
|
||
// 创建签到记录
|
||
checkIn := &model.MeetingCheckIn{
|
||
CheckInID: checkInID,
|
||
MeetingID: req.MeetingID,
|
||
UserID: userID,
|
||
CheckInTime: time.Now(),
|
||
}
|
||
|
||
if err := m.meetingCheckInDB.Create(c, checkIn); err != nil {
|
||
log.ZError(c, "CheckInMeeting: failed to create check-in", err, "meetingID", req.MeetingID, "userID", userID)
|
||
apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to create check-in"))
|
||
return
|
||
}
|
||
|
||
// 更新会议的签到统计
|
||
checkInCount, err := m.meetingCheckInDB.CountByMeetingID(c, req.MeetingID)
|
||
if err != nil {
|
||
log.ZWarn(c, "CheckInMeeting: failed to count check-ins", err, "meetingID", req.MeetingID)
|
||
// 不阻断流程,继续返回成功
|
||
} else {
|
||
if err := m.meetingDB.Update(c, req.MeetingID, map[string]any{"check_in_count": int32(checkInCount)}); err != nil {
|
||
log.ZWarn(c, "CheckInMeeting: failed to update meeting check-in count", err, "meetingID", req.MeetingID)
|
||
// 不阻断流程,继续返回成功
|
||
}
|
||
}
|
||
|
||
resp.CheckInID = checkInID
|
||
resp.CheckInTime = checkIn.CheckInTime.UnixMilli()
|
||
|
||
log.ZInfo(c, "CheckInMeeting: success", "meetingID", req.MeetingID, "userID", userID, "checkInID", checkInID)
|
||
apiresp.GinSuccess(c, resp)
|
||
}
|
||
|
||
// GetMeetingCheckIns 获取会议签到列表
|
||
func (m *MeetingApi) GetMeetingCheckIns(c *gin.Context) {
|
||
var (
|
||
req apistruct.GetMeetingCheckInsReq
|
||
resp apistruct.GetMeetingCheckInsResp
|
||
)
|
||
if err := c.BindJSON(&req); err != nil {
|
||
apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())
|
||
return
|
||
}
|
||
|
||
// 设置默认分页参数
|
||
if req.Pagination.PageNumber <= 0 {
|
||
req.Pagination.PageNumber = 1
|
||
}
|
||
if req.Pagination.ShowNumber <= 0 {
|
||
req.Pagination.ShowNumber = 20
|
||
}
|
||
|
||
// 创建分页对象
|
||
pagination := &meetingPaginationWrapper{
|
||
pageNumber: req.Pagination.PageNumber,
|
||
showNumber: req.Pagination.ShowNumber,
|
||
}
|
||
|
||
// 查询签到列表
|
||
total, checkIns, err := m.meetingCheckInDB.FindByMeetingID(c, req.MeetingID, pagination)
|
||
if err != nil {
|
||
log.ZError(c, "GetMeetingCheckIns: failed to find check-ins", err, "meetingID", req.MeetingID)
|
||
apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to find check-ins"))
|
||
return
|
||
}
|
||
|
||
// 收集所有用户ID
|
||
userIDs := make([]string, 0, len(checkIns))
|
||
for _, checkIn := range checkIns {
|
||
userIDs = append(userIDs, checkIn.UserID)
|
||
}
|
||
|
||
// 批量获取用户信息
|
||
userMap := make(map[string]*sdkws.UserInfo)
|
||
if len(userIDs) > 0 {
|
||
users, err := m.userClient.GetUsersInfo(c, userIDs)
|
||
if err != nil {
|
||
log.ZWarn(c, "GetMeetingCheckIns: failed to get users info", err, "userIDs", userIDs)
|
||
// 不阻断流程,继续返回签到列表,只是没有用户信息
|
||
} else {
|
||
for _, user := range users {
|
||
userMap[user.UserID] = user
|
||
}
|
||
}
|
||
}
|
||
|
||
// 转换为响应格式
|
||
checkInInfos := make([]*apistruct.MeetingCheckInInfo, 0, len(checkIns))
|
||
for _, checkIn := range checkIns {
|
||
checkInInfo := &apistruct.MeetingCheckInInfo{
|
||
CheckInID: checkIn.CheckInID,
|
||
MeetingID: checkIn.MeetingID,
|
||
UserID: checkIn.UserID,
|
||
CheckInTime: checkIn.CheckInTime.UnixMilli(),
|
||
}
|
||
if user, ok := userMap[checkIn.UserID]; ok {
|
||
checkInInfo.UserInfo = user
|
||
}
|
||
checkInInfos = append(checkInInfos, checkInInfo)
|
||
}
|
||
|
||
resp.Total = total
|
||
resp.CheckIns = checkInInfos
|
||
|
||
log.ZInfo(c, "GetMeetingCheckIns: success", "meetingID", req.MeetingID, "total", total, "count", len(checkInInfos))
|
||
apiresp.GinSuccess(c, resp)
|
||
}
|
||
|
||
// GetMeetingCheckInStats 获取会议签到统计
|
||
func (m *MeetingApi) GetMeetingCheckInStats(c *gin.Context) {
|
||
var (
|
||
req apistruct.GetMeetingCheckInStatsReq
|
||
resp apistruct.GetMeetingCheckInStatsResp
|
||
)
|
||
if err := c.BindJSON(&req); err != nil {
|
||
apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())
|
||
return
|
||
}
|
||
|
||
// 查询签到统计
|
||
checkInCount, err := m.meetingCheckInDB.CountByMeetingID(c, req.MeetingID)
|
||
if err != nil {
|
||
log.ZError(c, "GetMeetingCheckInStats: failed to count check-ins", err, "meetingID", req.MeetingID)
|
||
apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to count check-ins"))
|
||
return
|
||
}
|
||
|
||
resp.MeetingID = req.MeetingID
|
||
resp.CheckInCount = checkInCount
|
||
|
||
log.ZInfo(c, "GetMeetingCheckInStats: success", "meetingID", req.MeetingID, "checkInCount", checkInCount)
|
||
apiresp.GinSuccess(c, resp)
|
||
}
|
||
|
||
// CheckUserCheckIn 检查用户是否已签到
|
||
func (m *MeetingApi) CheckUserCheckIn(c *gin.Context) {
|
||
var (
|
||
req apistruct.CheckUserCheckInReq
|
||
resp apistruct.CheckUserCheckInResp
|
||
)
|
||
if err := c.BindJSON(&req); err != nil {
|
||
apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())
|
||
return
|
||
}
|
||
|
||
// 获取当前用户ID
|
||
userID := mcontext.GetOpUserID(c)
|
||
if userID == "" {
|
||
apiresp.GinError(c, errs.ErrArgs.WrapMsg("user id not found"))
|
||
return
|
||
}
|
||
|
||
// 查询用户是否已签到
|
||
checkIn, err := m.meetingCheckInDB.FindByUserAndMeetingID(c, userID, req.MeetingID)
|
||
if err != nil {
|
||
if m.IsNotFound(err) {
|
||
// 未签到
|
||
resp.IsCheckedIn = false
|
||
log.ZInfo(c, "CheckUserCheckIn: user not checked in", "meetingID", req.MeetingID, "userID", userID)
|
||
apiresp.GinSuccess(c, resp)
|
||
return
|
||
}
|
||
log.ZError(c, "CheckUserCheckIn: failed to find check-in", err, "meetingID", req.MeetingID, "userID", userID)
|
||
apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to find check-in"))
|
||
return
|
||
}
|
||
|
||
// 已签到,获取用户信息
|
||
var userInfo *sdkws.UserInfo
|
||
users, err := m.userClient.GetUsersInfo(c, []string{userID})
|
||
if err != nil {
|
||
log.ZWarn(c, "CheckUserCheckIn: failed to get user info", err, "userID", userID)
|
||
// 不阻断流程,继续返回签到信息,只是没有用户详细信息
|
||
} else if len(users) > 0 {
|
||
userInfo = users[0]
|
||
}
|
||
|
||
resp.IsCheckedIn = true
|
||
resp.CheckInInfo = &apistruct.MeetingCheckInInfo{
|
||
CheckInID: checkIn.CheckInID,
|
||
MeetingID: checkIn.MeetingID,
|
||
UserID: checkIn.UserID,
|
||
CheckInTime: checkIn.CheckInTime.UnixMilli(),
|
||
UserInfo: userInfo,
|
||
}
|
||
|
||
log.ZInfo(c, "CheckUserCheckIn: success", "meetingID", req.MeetingID, "userID", userID, "isCheckedIn", true)
|
||
apiresp.GinSuccess(c, resp)
|
||
}
|