Files
open-im-server-deploy/internal/api/meeting.go
kim.dev.6789 e50142a3b9 复制项目
2026-01-14 22:16:44 +08:00

1372 lines
44 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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)
}