签入签出
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
authModel "github.com/echochat/backend/app/auth/model"
|
||||
"github.com/echochat/backend/app/constants"
|
||||
"github.com/echochat/backend/app/dto"
|
||||
"github.com/echochat/backend/app/meeting/dao"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
notifyService "github.com/echochat/backend/app/notify/service"
|
||||
"github.com/echochat/backend/pkg/logs"
|
||||
"github.com/echochat/backend/pkg/utils"
|
||||
"github.com/google/uuid"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
@@ -46,7 +48,9 @@ var (
|
||||
ErrResourceNotOwned = errors.New("媒体资源归属校验失败,禁止操作他人资源")
|
||||
// ErrMediaServiceUnavailable 媒体服务当前不可用(Router 创建失败 / Node 宕机等)
|
||||
// 用于 CreateRoom / JoinRoom 的补偿路径,将前台错误与"用户输入错误"区分开
|
||||
ErrMediaServiceUnavailable = errors.New("媒体服务暂时不可用,请稍后重试")
|
||||
ErrMediaServiceUnavailable = errors.New("媒体服务暂时不可用,请稍后重试")
|
||||
ErrCloudLawCallerPhoneRequired = errors.New("发起人手机号不能为空")
|
||||
ErrCloudLawCallerPhoneInvalid = errors.New("发起人手机号长度超过限制")
|
||||
// Task 16 P2-7:会议聊天服务端限流
|
||||
ErrChatContentEmpty = errors.New("消息内容不能为空")
|
||||
ErrChatContentTooLong = errors.New("消息长度超过上限")
|
||||
@@ -249,13 +253,112 @@ func (s *MeetingService) ResolveUsersDisplay(ctx context.Context, userIDs []int6
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *MeetingService) EnsureCloudLawUserByPhone(ctx context.Context, phone string, nickname string) (*authModel.User, bool, error) {
|
||||
funcName := "service.meeting_service.EnsureCloudLawUserByPhone"
|
||||
phone = strings.TrimSpace(phone)
|
||||
nickname = strings.TrimSpace(nickname)
|
||||
if phone == "" {
|
||||
return nil, false, ErrCloudLawCallerPhoneRequired
|
||||
}
|
||||
if utf8.RuneCountInString(phone) > 20 {
|
||||
return nil, false, ErrCloudLawCallerPhoneInvalid
|
||||
}
|
||||
|
||||
var existing authModel.User
|
||||
err := s.db.WithContext(ctx).Where("phone = ?", phone).Order("id ASC").First(&existing).Error
|
||||
if err == nil {
|
||||
return &existing, false, nil
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
username := cloudLawUsername(phone)
|
||||
passwordHash, err := utils.HashPassword(uuid.New().String())
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
user := &authModel.User{
|
||||
Username: username,
|
||||
Email: username + "@cloud-law.local",
|
||||
PasswordHash: passwordHash,
|
||||
Nickname: cloudLawNickname(nickname, phone),
|
||||
Phone: &phone,
|
||||
Status: constants.UserStatusActive,
|
||||
}
|
||||
if err = s.db.WithContext(ctx).Create(user).Error; err != nil {
|
||||
var retry authModel.User
|
||||
if retryErr := s.db.WithContext(ctx).Where("phone = ?", phone).Order("id ASC").First(&retry).Error; retryErr == nil {
|
||||
return &retry, false, nil
|
||||
}
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
s.assignCloudLawDefaultRole(ctx, user.ID)
|
||||
logs.Info(ctx, funcName, "创建云律 EchoChat 用户成功",
|
||||
zap.Int64("user_id", user.ID),
|
||||
zap.String("phone", logs.MaskPhone(phone)),
|
||||
)
|
||||
return user, true, nil
|
||||
}
|
||||
|
||||
func (s *MeetingService) assignCloudLawDefaultRole(ctx context.Context, userID int64) {
|
||||
funcName := "service.meeting_service.assignCloudLawDefaultRole"
|
||||
var role authModel.Role
|
||||
if err := s.db.WithContext(ctx).Where("code = ?", constants.RoleUser).First(&role).Error; err != nil {
|
||||
logs.Warn(ctx, funcName, "查找默认角色失败,跳过角色分配", zap.Int64("user_id", userID), zap.Error(err))
|
||||
return
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Create(&authModel.UserRole{UserID: userID, RoleID: role.ID}).Error; err != nil {
|
||||
logs.Warn(ctx, funcName, "分配默认角色失败", zap.Int64("user_id", userID), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func cloudLawUsername(phone string) string {
|
||||
accountKey := strings.NewReplacer("+", "", "-", "", " ", "", "(", "", ")").Replace(phone)
|
||||
accountKey = strings.TrimSpace(accountKey)
|
||||
if accountKey == "" {
|
||||
accountKey = strings.ReplaceAll(uuid.New().String(), "-", "")[:12]
|
||||
}
|
||||
return "cloudlaw_" + accountKey
|
||||
}
|
||||
|
||||
func cloudLawNickname(nickname string, phone string) string {
|
||||
if nickname == "" {
|
||||
nickname = "云律用户" + cloudLawPhoneSuffix(phone)
|
||||
}
|
||||
return truncateCloudLawText(nickname, 50)
|
||||
}
|
||||
|
||||
func cloudLawPhoneSuffix(phone string) string {
|
||||
runes := []rune(phone)
|
||||
if len(runes) <= 4 {
|
||||
return phone
|
||||
}
|
||||
return string(runes[len(runes)-4:])
|
||||
}
|
||||
|
||||
func truncateCloudLawText(value string, limit int) string {
|
||||
if limit <= 0 || utf8.RuneCountInString(value) <= limit {
|
||||
return value
|
||||
}
|
||||
runes := []rune(value)
|
||||
return string(runes[:limit])
|
||||
}
|
||||
|
||||
// ====== 会议生命周期 ======
|
||||
|
||||
// CreateRoom 创建即时会议
|
||||
// 流程:生成唯一会议号 → bcrypt 密码 → 写入 meeting_rooms(status=Active + started_at=now)→ 主持人落 participant 表 → 驱动 mediasoup Router 创建
|
||||
func (s *MeetingService) CreateRoom(ctx context.Context, hostID int64, req *dto.CreateMeetingRoomRequest) (*model.MeetingRoom, *model.MeetingParticipant, string, error) {
|
||||
funcName := "service.meeting_service.CreateRoom"
|
||||
return s.createRoom(ctx, hostID, req, true, true, "service.meeting_service.CreateRoom")
|
||||
}
|
||||
|
||||
func (s *MeetingService) CreateRoomForInternal(ctx context.Context, hostID int64, req *dto.CreateMeetingRoomRequest) (*model.MeetingRoom, *model.MeetingParticipant, string, error) {
|
||||
return s.createRoom(ctx, hostID, req, false, false, "service.meeting_service.CreateRoomForInternal")
|
||||
}
|
||||
|
||||
func (s *MeetingService) createRoom(ctx context.Context, hostID int64, req *dto.CreateMeetingRoomRequest, enforceSingleActive bool, addHostParticipant bool, funcName string) (*model.MeetingRoom, *model.MeetingParticipant, string, error) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
@@ -263,13 +366,15 @@ func (s *MeetingService) CreateRoom(ctx context.Context, hostID int64, req *dto.
|
||||
}
|
||||
}()
|
||||
|
||||
active, err := s.participantDAO.FindActiveByUser(ctx, hostID)
|
||||
if err != nil {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
if active != nil {
|
||||
err = ErrAlreadyInOtherMeeting
|
||||
return nil, nil, "", err
|
||||
if enforceSingleActive {
|
||||
active, err := s.participantDAO.FindActiveByUser(ctx, hostID)
|
||||
if err != nil {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
if active != nil {
|
||||
err = ErrAlreadyInOtherMeeting
|
||||
return nil, nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
code, err := s.generateUniqueRoomCode(ctx)
|
||||
@@ -308,9 +413,12 @@ func (s *MeetingService) CreateRoom(ctx context.Context, hostID int64, req *dto.
|
||||
return nil, nil, "", err
|
||||
}
|
||||
|
||||
participant, err := s.participantDAO.JoinRoom(ctx, room.ID, hostID, constants.MeetingRoleHost)
|
||||
if err != nil {
|
||||
return nil, nil, "", err
|
||||
var participant *model.MeetingParticipant
|
||||
if addHostParticipant {
|
||||
participant, err = s.participantDAO.JoinRoom(ctx, room.ID, hostID, constants.MeetingRoleHost)
|
||||
if err != nil {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
// Task 7 起 CreateRouter 对接真实 mediasoup;Task 8 起仅在会议首次创建时调一次(房间级资源)
|
||||
@@ -323,9 +431,11 @@ func (s *MeetingService) CreateRoom(ctx context.Context, hostID int64, req *dto.
|
||||
logs.Warn(ctx, funcName, "mediasoup Router 创建失败,执行补偿回滚",
|
||||
zap.String("room_code", code), zap.Int64("host_id", hostID), zap.Error(mediaErr))
|
||||
|
||||
if _, leaveErr := s.participantDAO.LeaveRoom(ctx, room.ID, hostID, constants.MeetingLeftReasonSelf); leaveErr != nil {
|
||||
logs.Warn(ctx, funcName, "补偿阶段 LeaveRoom 失败(已记录,清理任务会兜底)",
|
||||
zap.Int64("room_id", room.ID), zap.Int64("host_id", hostID), zap.Error(leaveErr))
|
||||
if participant != nil {
|
||||
if _, leaveErr := s.participantDAO.LeaveRoom(ctx, room.ID, hostID, constants.MeetingLeftReasonSelf); leaveErr != nil {
|
||||
logs.Warn(ctx, funcName, "补偿阶段 LeaveRoom 失败(已记录,清理任务会兜底)",
|
||||
zap.Int64("room_id", room.ID), zap.Int64("host_id", hostID), zap.Error(leaveErr))
|
||||
}
|
||||
}
|
||||
if _, markErr := s.roomDAO.MarkEnded(ctx, room.ID, constants.MeetingEndedReasonSystemError, time.Now()); markErr != nil {
|
||||
logs.Warn(ctx, funcName, "补偿阶段 MarkEnded 失败(已记录,清理任务会兜底)",
|
||||
|
||||
Reference in New Issue
Block a user