635 lines
20 KiB
Go
635 lines
20 KiB
Go
// Package controller 提供 meeting 模块的 HTTP 接口
|
||
package controller
|
||
|
||
import (
|
||
"errors"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/echochat/backend/app/constants"
|
||
"github.com/echochat/backend/app/dto"
|
||
"github.com/echochat/backend/app/meeting/model"
|
||
"github.com/echochat/backend/app/meeting/service"
|
||
"github.com/echochat/backend/config"
|
||
"github.com/echochat/backend/pkg/logs"
|
||
"github.com/echochat/backend/pkg/middleware"
|
||
"github.com/echochat/backend/pkg/utils"
|
||
"github.com/gin-gonic/gin"
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
// MeetingController 会议 REST 控制器
|
||
// Task 5 完成:填充 12 个接口的请求解析、业务调用、DTO 转换、错误码映射
|
||
type MeetingController struct {
|
||
meetingService *service.MeetingService
|
||
meetingConfig *config.MeetingConfig
|
||
}
|
||
|
||
// NewMeetingController 创建 MeetingController 实例
|
||
func NewMeetingController(meetingService *service.MeetingService, meetingConfig *config.MeetingConfig) *MeetingController {
|
||
return &MeetingController{meetingService: meetingService, meetingConfig: meetingConfig}
|
||
}
|
||
|
||
type cloudLawCreateMeetingRequest struct {
|
||
CallID string `json:"callId"`
|
||
CallerID int64 `json:"callerId"`
|
||
CallerPhone string `json:"callerPhone"`
|
||
CallerName string `json:"callerName"`
|
||
ReceiverID int64 `json:"receiverId"`
|
||
ReceiverType string `json:"receiverType"`
|
||
Title string `json:"title"`
|
||
CallType string `json:"callType"`
|
||
}
|
||
|
||
type cloudLawJoinMeetingRequest struct {
|
||
ReceiverID int64 `json:"receiverId"`
|
||
ReceiverType string `json:"receiverType"`
|
||
ReceiverPhone string `json:"receiverPhone"`
|
||
ReceiverName string `json:"receiverName"`
|
||
CallID string `json:"callId"`
|
||
}
|
||
|
||
func (ctl *MeetingController) requireCloudLawInternalToken(c *gin.Context) bool {
|
||
expected := ""
|
||
if ctl.meetingConfig != nil {
|
||
expected = ctl.meetingConfig.CloudLawInternalToken
|
||
}
|
||
if expected == "" {
|
||
utils.ResponseForbidden(c, "云律内部接口未配置")
|
||
return false
|
||
}
|
||
if c.GetHeader("X-Internal-Token") != expected {
|
||
utils.ResponseUnauthorized(c, "内部访问令牌无效")
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
func cloudLawMeetingPayload(room *model.MeetingRoom, onlineCount int64) gin.H {
|
||
return gin.H{
|
||
"roomId": strconv.FormatInt(room.ID, 10),
|
||
"roomCode": room.RoomCode,
|
||
"code": room.RoomCode,
|
||
"meetingCode": room.RoomCode,
|
||
"status": room.Status,
|
||
"onlineCount": onlineCount,
|
||
}
|
||
}
|
||
|
||
// requireUserID 统一的当前用户取值,失败直接写 401 并返回 false
|
||
func requireUserID(c *gin.Context) (int64, bool) {
|
||
userID, ok := middleware.GetCurrentUserID(c)
|
||
if !ok {
|
||
utils.ResponseUnauthorized(c, "无法获取当前用户信息")
|
||
return 0, false
|
||
}
|
||
return userID, true
|
||
}
|
||
|
||
// handleError 将 service 层领域错误统一映射为 HTTP 响应
|
||
// 映射原则:
|
||
// - 资源不存在 → 404
|
||
// - 权限不足(非 host)→ 403
|
||
// - 业务规则违反(密码错误、已满、状态冲突等)→ 400
|
||
// - 其余未知错误 → 500
|
||
func (ctl *MeetingController) handleError(c *gin.Context, err error, fallbackMsg string) {
|
||
switch {
|
||
case errors.Is(err, service.ErrMeetingNotFound):
|
||
utils.ResponseNotFound(c, err.Error())
|
||
case errors.Is(err, service.ErrNotMeetingHost):
|
||
utils.ResponseForbidden(c, err.Error())
|
||
case errors.Is(err, service.ErrMediaServiceUnavailable):
|
||
// P0-3:媒体服务不可用应作为 5xx 返回,让前端走"稍后重试"提示,而非按"用户输入错误"处理
|
||
utils.ResponseError(c, err.Error())
|
||
case errors.Is(err, service.ErrMeetingEnded),
|
||
errors.Is(err, service.ErrMeetingFull),
|
||
errors.Is(err, service.ErrMeetingPasswordReq),
|
||
errors.Is(err, service.ErrMeetingPasswordWrong),
|
||
errors.Is(err, service.ErrMeetingPasswordLocked),
|
||
errors.Is(err, service.ErrNotInMeeting),
|
||
errors.Is(err, service.ErrAlreadyInMeeting),
|
||
errors.Is(err, service.ErrAlreadyInOtherMeeting),
|
||
errors.Is(err, service.ErrInviteTokenInvalid),
|
||
errors.Is(err, service.ErrRoomCodeConflict),
|
||
errors.Is(err, service.ErrKickSelfForbidden),
|
||
errors.Is(err, service.ErrTransferToSelf),
|
||
errors.Is(err, service.ErrTransferTargetInvalid),
|
||
errors.Is(err, service.ErrCloudLawCallerPhoneRequired),
|
||
errors.Is(err, service.ErrCloudLawCallerPhoneInvalid),
|
||
// P2-7 会议聊天服务端校验失败:内容为空 / 超长 / 触发限流
|
||
// 当前使用 400(由 ResponseBadRequest 返回),保持与其余业务错误一致;
|
||
// 未来若需要精细区分(如 ErrChatRateLimited → 429、ErrChatContentTooLong → 413),可拆分分支
|
||
errors.Is(err, service.ErrChatContentEmpty),
|
||
errors.Is(err, service.ErrChatContentTooLong),
|
||
errors.Is(err, service.ErrChatRateLimited):
|
||
utils.ResponseBadRequest(c, err.Error())
|
||
default:
|
||
logs.Warn(c.Request.Context(), "controller.meeting_controller.handleError",
|
||
fallbackMsg, zap.Error(err))
|
||
utils.ResponseError(c, fallbackMsg)
|
||
}
|
||
}
|
||
|
||
// ====== DTO 转换 ======
|
||
|
||
// roomToDTO 将 model.MeetingRoom 转 DTO,host_name/avatar 交由上层按需补全
|
||
func roomToDTO(r *model.MeetingRoom, onlineCount int) *dto.MeetingRoomDTO {
|
||
if r == nil {
|
||
return nil
|
||
}
|
||
out := &dto.MeetingRoomDTO{
|
||
ID: r.ID,
|
||
RoomCode: r.RoomCode,
|
||
Title: r.Title,
|
||
HostID: r.HostID,
|
||
Type: r.Type,
|
||
HasPassword: r.PasswordHash != nil && *r.PasswordHash != "",
|
||
MaxMembers: r.MaxMembers,
|
||
Status: r.Status,
|
||
StatusLabel: constants.MeetingStatusMap[r.Status],
|
||
Settings: r.Settings,
|
||
CreatedAt: r.CreatedAt.Format("2006-01-02 15:04:05"),
|
||
OnlineCount: onlineCount,
|
||
EndedReason: r.EndedReason,
|
||
}
|
||
if r.ScheduledAt != nil {
|
||
out.ScheduledAt = r.ScheduledAt.Format("2006-01-02 15:04:05")
|
||
}
|
||
if r.StartedAt != nil {
|
||
out.StartedAt = r.StartedAt.Format("2006-01-02 15:04:05")
|
||
}
|
||
if r.EndedAt != nil {
|
||
out.EndedAt = r.EndedAt.Format("2006-01-02 15:04:05")
|
||
}
|
||
return out
|
||
}
|
||
|
||
// participantToDTO 将 model.MeetingParticipant 转 DTO
|
||
// userMap 可选:若提供则附带 user_name / user_avatar,便于前端 MemberPanel/VideoTile 直接渲染昵称头像
|
||
func participantToDTO(p *model.MeetingParticipant, userMap map[int64]service.UserDisplayInfo) *dto.MeetingParticipantDTO {
|
||
if p == nil {
|
||
return nil
|
||
}
|
||
out := &dto.MeetingParticipantDTO{
|
||
ID: p.ID,
|
||
RoomID: p.RoomID,
|
||
UserID: p.UserID,
|
||
Role: p.Role,
|
||
RoleLabel: constants.MeetingRoleMap[p.Role],
|
||
JoinedAt: p.JoinedAt.Format("2006-01-02 15:04:05"),
|
||
LeftReason: p.LeftReason,
|
||
Duration: p.Duration,
|
||
IsActive: p.IsActive(),
|
||
}
|
||
if p.LeftAt != nil {
|
||
out.LeftAt = p.LeftAt.Format("2006-01-02 15:04:05")
|
||
}
|
||
if info, ok := userMap[p.UserID]; ok {
|
||
out.UserName = info.Name
|
||
out.UserAvatar = info.Avatar
|
||
}
|
||
return out
|
||
}
|
||
|
||
// chatToDTO 将 model.MeetingChat 转 DTO
|
||
// userMap 可选:若提供则附带 user_name / user_avatar,否则为空
|
||
func chatToDTO(m *model.MeetingChat, userMap map[int64]service.UserDisplayInfo) *dto.MeetingChatDTO {
|
||
if m == nil {
|
||
return nil
|
||
}
|
||
d := &dto.MeetingChatDTO{
|
||
ID: m.ID,
|
||
RoomID: m.RoomID,
|
||
UserID: m.UserID,
|
||
Content: m.Content,
|
||
CreatedAt: m.CreatedAt.Format("2006-01-02 15:04:05"),
|
||
}
|
||
if info, ok := userMap[m.UserID]; ok {
|
||
d.UserName = info.Name
|
||
d.UserAvatar = info.Avatar
|
||
}
|
||
return d
|
||
}
|
||
|
||
// ====== REST API ======
|
||
|
||
func (ctl *MeetingController) CloudLawCreateMeeting(c *gin.Context) {
|
||
if !ctl.requireCloudLawInternalToken(c) {
|
||
return
|
||
}
|
||
var req cloudLawCreateMeetingRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
|
||
return
|
||
}
|
||
if req.CallerID <= 0 {
|
||
utils.ResponseBadRequest(c, "callerId不能为空")
|
||
return
|
||
}
|
||
req.CallerPhone = strings.TrimSpace(req.CallerPhone)
|
||
req.CallerName = strings.TrimSpace(req.CallerName)
|
||
if req.CallerPhone == "" {
|
||
utils.ResponseBadRequest(c, "callerPhone不能为空")
|
||
return
|
||
}
|
||
title := req.Title
|
||
if title == "" {
|
||
title = "云律立即连线"
|
||
}
|
||
hostUser, created, err := ctl.meetingService.EnsureCloudLawUserByPhone(c.Request.Context(), req.CallerPhone, req.CallerName)
|
||
if err != nil {
|
||
ctl.handleError(c, err, "创建云律会议主持人失败")
|
||
return
|
||
}
|
||
room, _, _, err := ctl.meetingService.CreateRoomForInternal(c.Request.Context(), hostUser.ID, &dto.CreateMeetingRoomRequest{
|
||
Title: title,
|
||
MaxMembers: 2,
|
||
})
|
||
if err != nil {
|
||
ctl.handleError(c, err, "创建云律会议失败")
|
||
return
|
||
}
|
||
payload := cloudLawMeetingPayload(room, 0)
|
||
payload["callId"] = req.CallID
|
||
payload["callerId"] = req.CallerID
|
||
payload["callerPhone"] = req.CallerPhone
|
||
payload["echoChatHostUserId"] = strconv.FormatInt(hostUser.ID, 10)
|
||
payload["echoChatHostUserCreated"] = created
|
||
payload["receiverId"] = req.ReceiverID
|
||
payload["receiverType"] = req.ReceiverType
|
||
payload["callType"] = req.CallType
|
||
utils.ResponseOK(c, payload)
|
||
}
|
||
|
||
func (ctl *MeetingController) CloudLawGetMeeting(c *gin.Context) {
|
||
if !ctl.requireCloudLawInternalToken(c) {
|
||
return
|
||
}
|
||
code := c.Param("code")
|
||
if code == "" {
|
||
utils.ResponseBadRequest(c, "会议号不能为空")
|
||
return
|
||
}
|
||
room, _, onlineCount, err := ctl.meetingService.GetRoomForInternal(c.Request.Context(), code)
|
||
if err != nil {
|
||
ctl.handleError(c, err, "获取云律会议详情失败")
|
||
return
|
||
}
|
||
utils.ResponseOK(c, cloudLawMeetingPayload(room, onlineCount))
|
||
}
|
||
|
||
func (ctl *MeetingController) CloudLawJoinMeeting(c *gin.Context) {
|
||
if !ctl.requireCloudLawInternalToken(c) {
|
||
return
|
||
}
|
||
code := c.Param("code")
|
||
if code == "" {
|
||
utils.ResponseBadRequest(c, "会议号不能为空")
|
||
return
|
||
}
|
||
var req cloudLawJoinMeetingRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
|
||
return
|
||
}
|
||
req.ReceiverPhone = strings.TrimSpace(req.ReceiverPhone)
|
||
req.ReceiverName = strings.TrimSpace(req.ReceiverName)
|
||
if req.ReceiverPhone == "" {
|
||
utils.ResponseBadRequest(c, "receiverPhone不能为空")
|
||
return
|
||
}
|
||
user, created, err := ctl.meetingService.EnsureCloudLawUserByPhone(c.Request.Context(), req.ReceiverPhone, req.ReceiverName)
|
||
if err != nil {
|
||
ctl.handleError(c, err, "创建云律会议接单人失败")
|
||
return
|
||
}
|
||
room, participant, _, err := ctl.meetingService.JoinRoomForInternal(c.Request.Context(), user.ID, code)
|
||
if err != nil {
|
||
ctl.handleError(c, err, "云律接单人加入会议失败")
|
||
return
|
||
}
|
||
payload := cloudLawMeetingPayload(room, 0)
|
||
payload["callId"] = req.CallID
|
||
payload["receiverId"] = req.ReceiverID
|
||
payload["receiverType"] = req.ReceiverType
|
||
payload["echoChatParticipantUserId"] = strconv.FormatInt(user.ID, 10)
|
||
payload["echoChatParticipantUserCreated"] = created
|
||
if participant != nil {
|
||
payload["participantId"] = strconv.FormatInt(participant.ID, 10)
|
||
}
|
||
utils.ResponseOK(c, payload)
|
||
}
|
||
|
||
func (ctl *MeetingController) CloudLawEndMeeting(c *gin.Context) {
|
||
if !ctl.requireCloudLawInternalToken(c) {
|
||
return
|
||
}
|
||
code := c.Param("code")
|
||
if code == "" {
|
||
utils.ResponseBadRequest(c, "会议号不能为空")
|
||
return
|
||
}
|
||
if err := ctl.meetingService.EndRoomForInternal(c.Request.Context(), code); err != nil {
|
||
ctl.handleError(c, err, "结束云律会议失败")
|
||
return
|
||
}
|
||
utils.ResponseOK(c, gin.H{"roomCode": code, "code": code, "meetingCode": code})
|
||
}
|
||
|
||
// CreateRoom POST /api/v1/meeting/rooms
|
||
func (ctl *MeetingController) CreateRoom(c *gin.Context) {
|
||
userID, ok := requireUserID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
var req dto.CreateMeetingRoomRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
|
||
return
|
||
}
|
||
room, _, routerID, err := ctl.meetingService.CreateRoom(c.Request.Context(), userID, &req)
|
||
if err != nil {
|
||
ctl.handleError(c, err, "创建会议失败")
|
||
return
|
||
}
|
||
// Task 9:CreateRoom 响应携带 router_id + rtpCapabilities,供前端 Device.load 使用
|
||
_, rtpCaps, _ := ctl.meetingService.ResolveRouterInfo(room.RoomCode)
|
||
resp := dto.CreateMeetingRoomResponse{
|
||
Room: *roomToDTO(room, 1),
|
||
RouterID: routerID,
|
||
RtpCapabilities: rtpCaps,
|
||
}
|
||
utils.ResponseCreated(c, resp)
|
||
}
|
||
|
||
// GetRoom GET /api/v1/meeting/rooms/:code
|
||
func (ctl *MeetingController) GetRoom(c *gin.Context) {
|
||
userID, ok := requireUserID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
code := c.Param("code")
|
||
if code == "" {
|
||
utils.ResponseBadRequest(c, "会议号不能为空")
|
||
return
|
||
}
|
||
room, participants, onlineCount, err := ctl.meetingService.GetRoomByCode(c.Request.Context(), userID, code)
|
||
if err != nil {
|
||
ctl.handleError(c, err, "获取会议详情失败")
|
||
return
|
||
}
|
||
userIDs := make([]int64, 0, len(participants))
|
||
for i := range participants {
|
||
userIDs = append(userIDs, participants[i].UserID)
|
||
}
|
||
userMap := ctl.meetingService.ResolveUsersDisplay(c.Request.Context(), userIDs)
|
||
|
||
parts := make([]dto.MeetingParticipantDTO, 0, len(participants))
|
||
for i := range participants {
|
||
parts = append(parts, *participantToDTO(&participants[i], userMap))
|
||
}
|
||
resp := dto.GetMeetingRoomResponse{
|
||
Room: *roomToDTO(room, int(onlineCount)),
|
||
Participants: parts,
|
||
OnlineCount: int(onlineCount),
|
||
}
|
||
utils.ResponseOK(c, resp)
|
||
}
|
||
|
||
// JoinRoom POST /api/v1/meeting/rooms/:code/join
|
||
func (ctl *MeetingController) JoinRoom(c *gin.Context) {
|
||
userID, ok := requireUserID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
code := c.Param("code")
|
||
if code == "" {
|
||
utils.ResponseBadRequest(c, "会议号不能为空")
|
||
return
|
||
}
|
||
var req dto.JoinMeetingRoomRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil && err.Error() != "EOF" {
|
||
utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
|
||
return
|
||
}
|
||
room, participant, routerID, err := ctl.meetingService.JoinRoom(c.Request.Context(), userID, code, req.Password)
|
||
if err != nil {
|
||
ctl.handleError(c, err, "加入会议失败")
|
||
return
|
||
}
|
||
// Task 9:JoinRoom 响应携带 rtpCapabilities,供前端 mediasoup-client Device.load 直接使用
|
||
_, rtpCaps, _ := ctl.meetingService.ResolveRouterInfo(code)
|
||
userMap := ctl.meetingService.ResolveUsersDisplay(c.Request.Context(), []int64{participant.UserID})
|
||
resp := dto.JoinMeetingRoomResponse{
|
||
Room: *roomToDTO(room, 0),
|
||
Participant: *participantToDTO(participant, userMap),
|
||
RouterID: routerID,
|
||
RtpCapabilities: rtpCaps,
|
||
}
|
||
utils.ResponseOK(c, resp)
|
||
}
|
||
|
||
// LeaveRoom POST /api/v1/meeting/rooms/:code/leave
|
||
func (ctl *MeetingController) LeaveRoom(c *gin.Context) {
|
||
userID, ok := requireUserID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
code := c.Param("code")
|
||
if code == "" {
|
||
utils.ResponseBadRequest(c, "会议号不能为空")
|
||
return
|
||
}
|
||
duration, err := ctl.meetingService.LeaveRoom(c.Request.Context(), userID, code)
|
||
if err != nil {
|
||
ctl.handleError(c, err, "离开会议失败")
|
||
return
|
||
}
|
||
utils.ResponseOK(c, dto.LeaveMeetingRoomResponse{Duration: duration})
|
||
}
|
||
|
||
// EndRoom POST /api/v1/meeting/rooms/:code/end
|
||
func (ctl *MeetingController) EndRoom(c *gin.Context) {
|
||
userID, ok := requireUserID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
code := c.Param("code")
|
||
if code == "" {
|
||
utils.ResponseBadRequest(c, "会议号不能为空")
|
||
return
|
||
}
|
||
if err := ctl.meetingService.EndRoom(c.Request.Context(), userID, code); err != nil {
|
||
ctl.handleError(c, err, "结束会议失败")
|
||
return
|
||
}
|
||
utils.ResponseOK(c, gin.H{})
|
||
}
|
||
|
||
// ListMyMeetings GET /api/v1/meeting/rooms/mine?status=&before_id=&limit=
|
||
func (ctl *MeetingController) ListMyMeetings(c *gin.Context) {
|
||
userID, ok := requireUserID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
var req dto.ListMyMeetingsRequest
|
||
if err := c.ShouldBindQuery(&req); err != nil {
|
||
utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
|
||
return
|
||
}
|
||
rooms, hasMore, err := ctl.meetingService.ListMyMeetings(c.Request.Context(), userID, req.Status, req.BeforeID, req.Limit)
|
||
if err != nil {
|
||
ctl.handleError(c, err, "获取会议列表失败")
|
||
return
|
||
}
|
||
list := make([]dto.MeetingRoomDTO, 0, len(rooms))
|
||
for i := range rooms {
|
||
list = append(list, *roomToDTO(&rooms[i], 0))
|
||
}
|
||
utils.ResponseOK(c, dto.ListMyMeetingsResponse{List: list, HasMore: hasMore})
|
||
}
|
||
|
||
// TransferHost POST /api/v1/meeting/rooms/:code/transfer-host
|
||
func (ctl *MeetingController) TransferHost(c *gin.Context) {
|
||
userID, ok := requireUserID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
code := c.Param("code")
|
||
if code == "" {
|
||
utils.ResponseBadRequest(c, "会议号不能为空")
|
||
return
|
||
}
|
||
var req dto.TransferHostRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
|
||
return
|
||
}
|
||
if err := ctl.meetingService.TransferHost(c.Request.Context(), userID, code, req.TargetUserID); err != nil {
|
||
ctl.handleError(c, err, "转让主持人失败")
|
||
return
|
||
}
|
||
utils.ResponseOK(c, gin.H{})
|
||
}
|
||
|
||
// KickMember POST /api/v1/meeting/rooms/:code/kick
|
||
func (ctl *MeetingController) KickMember(c *gin.Context) {
|
||
userID, ok := requireUserID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
code := c.Param("code")
|
||
if code == "" {
|
||
utils.ResponseBadRequest(c, "会议号不能为空")
|
||
return
|
||
}
|
||
var req dto.KickMemberRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
|
||
return
|
||
}
|
||
if err := ctl.meetingService.KickMember(c.Request.Context(), userID, code, req.UserID); err != nil {
|
||
ctl.handleError(c, err, "踢出成员失败")
|
||
return
|
||
}
|
||
utils.ResponseOK(c, gin.H{})
|
||
}
|
||
|
||
// InviteUsers POST /api/v1/meeting/rooms/:code/invite
|
||
func (ctl *MeetingController) InviteUsers(c *gin.Context) {
|
||
userID, ok := requireUserID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
code := c.Param("code")
|
||
if code == "" {
|
||
utils.ResponseBadRequest(c, "会议号不能为空")
|
||
return
|
||
}
|
||
var req dto.InviteUsersRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
|
||
return
|
||
}
|
||
pushed, skipped, err := ctl.meetingService.InviteUsers(c.Request.Context(), userID, code, req.InviteeIDs)
|
||
if err != nil {
|
||
ctl.handleError(c, err, "发送邀请失败")
|
||
return
|
||
}
|
||
utils.ResponseOK(c, dto.InviteUsersResponse{Pushed: pushed, Skipped: skipped})
|
||
}
|
||
|
||
// RedeemInvite POST /api/v1/meeting/invite-tokens/:token/redeem
|
||
func (ctl *MeetingController) RedeemInvite(c *gin.Context) {
|
||
userID, ok := requireUserID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
token := c.Param("token")
|
||
if token == "" {
|
||
utils.ResponseBadRequest(c, "邀请 Token 不能为空")
|
||
return
|
||
}
|
||
resp, err := ctl.meetingService.RedeemInviteToken(c.Request.Context(), userID, token)
|
||
if err != nil {
|
||
ctl.handleError(c, err, "兑换邀请链接失败")
|
||
return
|
||
}
|
||
utils.ResponseOK(c, resp)
|
||
}
|
||
|
||
// SendChat POST /api/v1/meeting/rooms/:code/chats
|
||
func (ctl *MeetingController) SendChat(c *gin.Context) {
|
||
userID, ok := requireUserID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
code := c.Param("code")
|
||
if code == "" {
|
||
utils.ResponseBadRequest(c, "会议号不能为空")
|
||
return
|
||
}
|
||
var req dto.SendMeetingChatRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
|
||
return
|
||
}
|
||
msg, err := ctl.meetingService.SendChatMessage(c.Request.Context(), userID, code, req.Content)
|
||
if err != nil {
|
||
ctl.handleError(c, err, "发送会议聊天失败")
|
||
return
|
||
}
|
||
userMap := ctl.meetingService.ResolveUsersDisplay(c.Request.Context(), []int64{msg.UserID})
|
||
utils.ResponseCreated(c, dto.SendMeetingChatResponse{Message: *chatToDTO(msg, userMap)})
|
||
}
|
||
|
||
// ListChats GET /api/v1/meeting/rooms/:code/chats?before_id=&limit=
|
||
func (ctl *MeetingController) ListChats(c *gin.Context) {
|
||
userID, ok := requireUserID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
code := c.Param("code")
|
||
if code == "" {
|
||
utils.ResponseBadRequest(c, "会议号不能为空")
|
||
return
|
||
}
|
||
beforeID, _ := strconv.ParseInt(c.Query("before_id"), 10, 64)
|
||
limit, _ := strconv.Atoi(c.Query("limit"))
|
||
msgs, hasMore, err := ctl.meetingService.ListChatMessages(c.Request.Context(), userID, code, beforeID, limit)
|
||
if err != nil {
|
||
ctl.handleError(c, err, "获取会议聊天失败")
|
||
return
|
||
}
|
||
userIDs := make([]int64, 0, len(msgs))
|
||
for i := range msgs {
|
||
userIDs = append(userIDs, msgs[i].UserID)
|
||
}
|
||
userMap := ctl.meetingService.ResolveUsersDisplay(c.Request.Context(), userIDs)
|
||
list := make([]dto.MeetingChatDTO, 0, len(msgs))
|
||
for i := range msgs {
|
||
list = append(list, *chatToDTO(&msgs[i], userMap))
|
||
}
|
||
utils.ResponseOK(c, dto.ListMeetingChatsResponse{List: list, HasMore: hasMore})
|
||
}
|