From da66f6f6a11eabf2efca718050fff99d83456bd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E9=99=88=E9=91=AB?= <3240103237@zju.edu.cn> Date: Sat, 23 May 2026 15:34:21 +0800 Subject: [PATCH 1/3] feat: implement room creation and management (Part 5) --- internal/repository/room_repository.go | 22 + internal/service/room_service.go | 678 ++++++++++++++++++++++++- internal/service/user_service.go | 68 ++- 3 files changed, 741 insertions(+), 27 deletions(-) diff --git a/internal/repository/room_repository.go b/internal/repository/room_repository.go index aed528f..9f9d59d 100644 --- a/internal/repository/room_repository.go +++ b/internal/repository/room_repository.go @@ -237,6 +237,28 @@ func (r *RoomRepository) CountPendingJoinRequestsByUser(ctx context.Context, use return count, nil } +func (r *RoomRepository) DeleteMember(ctx context.Context, roomID, userID uint) error { + return r.db.WithContext(ctx). + Where("room_id = ? AND user_id = ?", roomID, userID). + Delete(&models.RoomMember{}).Error +} + +func (r *RoomRepository) GetPendingJoinRequestByRoomAndUser(ctx context.Context, roomID, userID uint) (*models.JoinRequest, error) { + var req models.JoinRequest + if err := r.db.WithContext(ctx).Where("room_id = ? AND user_id = ? AND status = ?", roomID, userID, "pending").First(&req).Error; err != nil { + return nil, err + } + return &req, nil +} + +func (r *RoomRepository) GetJoinRequestsByRoomID(ctx context.Context, roomID uint) ([]models.JoinRequest, error) { + var requests []models.JoinRequest + if err := r.db.WithContext(ctx).Preload("User").Where("room_id = ?", roomID).Order("created_at DESC").Find(&requests).Error; err != nil { + return nil, err + } + return requests, nil +} + /*为了避免多次查询数据库*/ func (r *RoomRepository) CountMembersByRoomIDs(ctx context.Context, roomIDs []uint) (map[uint]int64, error) { var results []struct { diff --git a/internal/service/room_service.go b/internal/service/room_service.go index fcb7589..85f0449 100644 --- a/internal/service/room_service.go +++ b/internal/service/room_service.go @@ -3,10 +3,12 @@ package service import ( "context" "fmt" + "strings" "time" "github.com/QSCTech/SRTP-Backend/internal/repository" "github.com/QSCTech/SRTP-Backend/models" + "github.com/QSCTech/SRTP-Backend/pkg/utils" "errors" "gorm.io/gorm" @@ -158,18 +160,6 @@ func (s *RoomService) List(ctx context.Context, input ListRoomsInput) (*ListRoom }, nil } -func (s *RoomService) ListMineCreated(ctx context.Context, page, pageSize int) (*ListRoomsOutput, error) { - return nil, fmt.Errorf("room service ListMineCreated not implemented") -} - -func (s *RoomService) ListMineJoined(ctx context.Context, page, pageSize int) (*ListRoomsOutput, error) { - return nil, fmt.Errorf("room service ListMineJoined not implemented") -} - -func (s *RoomService) GetMyStats(ctx context.Context) (*UserStatsOutput, error) { - return nil, fmt.Errorf("room service GetMyStats not implemented") -} - /*基础功能:拿数据、判断*/ func (s *RoomService) GetByPublicID(ctx context.Context, publicID string) (*models.Room, []models.RoomMember, error) { @@ -209,41 +199,685 @@ func (s *RoomService) GetByID(ctx context.Context, id uint) (*models.Room, []mod } func (s *RoomService) Create(ctx context.Context, input CreateRoomInput) (*models.Room, error) { - return nil, fmt.Errorf("room service Create not implemented") + currentUser, err := s.userService.GetCurrent(ctx) + if err != nil { + return nil, err + } + + name := utils.NormalizeWhitespace(input.Name) + if name == "" { + return nil, fmt.Errorf("name is required") + } + if strings.TrimSpace(input.SportType) == "" { + return nil, fmt.Errorf("sport_type is required") + } + if strings.TrimSpace(input.CampusName) == "" { + return nil, fmt.Errorf("campus_name is required") + } + if strings.TrimSpace(input.VenueName) == "" { + return nil, fmt.Errorf("venue_name is required") + } + if strings.TrimSpace(input.Visibility) == "" { + return nil, fmt.Errorf("visibility is required") + } + if strings.TrimSpace(input.JoinMode) == "" { + return nil, fmt.Errorf("join_mode is required") + } + if input.StartTime.IsZero() { + return nil, fmt.Errorf("start_time is required") + } + if input.EndTime.IsZero() { + return nil, fmt.Errorf("end_time is required") + } + + if !input.EndTime.After(input.StartTime) { + return nil, fmt.Errorf("end_time must be after start_time") + } + + if isBuddyCodeSport(input.SportType) { + if input.MemberLimit == nil || *input.MemberLimit < 2 { + return nil, fmt.Errorf("buddy-code sport requires at least 2 members") + } + } + + room := &models.Room{ + OwnerID: currentUser.ID, + Name: name, + SportType: strings.TrimSpace(input.SportType), + CampusName: utils.NormalizeWhitespace(input.CampusName), + VenueName: utils.NormalizeWhitespace(input.VenueName), + Visibility: strings.TrimSpace(input.Visibility), + JoinMode: strings.TrimSpace(input.JoinMode), + StartTime: input.StartTime, + EndTime: input.EndTime, + NeedReservation: input.NeedReservation, + InviteCode: generateInviteCode(), + } + if input.GenderRule != nil { + room.GenderRule = strings.TrimSpace(*input.GenderRule) + } + if input.MemberLimit != nil { + value := int(*input.MemberLimit) + room.MemberLimit = &value + } + if input.Organization != nil { + room.Organization = utils.NormalizeWhitespace(*input.Organization) + } + if input.LevelDesc != nil { + room.LevelDesc = strings.TrimSpace(*input.LevelDesc) + } + if input.Description != nil { + room.Description = strings.TrimSpace(*input.Description) + } + + if room.NeedReservation { + room.ReservationStatus = "pending" + } + + if err := s.repo.Create(ctx, room); err != nil { + return nil, err + } + + now := time.Now() + if err := s.repo.CreateMember(ctx, &models.RoomMember{ + RoomID: room.ID, + UserID: currentUser.ID, + Role: "owner", + Status: "joined", + JoinedAt: &now, + CreatedAt: now, + UpdatedAt: now, + }); err != nil { + return nil, err + } + + room.Owner = *currentUser + return room, nil } func (s *RoomService) Update(ctx context.Context, roomID uint, input UpdateRoomInput) (*models.Room, error) { - return nil, fmt.Errorf("room service Update not implemented") + currentUser, err := s.userService.GetCurrent(ctx) + if err != nil { + return nil, err + } + + room, err := s.repo.GetByID(ctx, roomID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("room not found") + } + return nil, err + } + + if room.OwnerID != currentUser.ID { + return nil, fmt.Errorf("only the owner can update the room") + } + + if room.Status != "recruiting" { + return nil, fmt.Errorf("room is not active") + } + + if input.Name != nil { + name := utils.NormalizeWhitespace(*input.Name) + if name == "" { + return nil, fmt.Errorf("name is required") + } + room.Name = name + } + if input.Visibility != nil { + room.Visibility = strings.TrimSpace(*input.Visibility) + } + if input.JoinMode != nil { + room.JoinMode = strings.TrimSpace(*input.JoinMode) + } + if input.StartTime != nil { + room.StartTime = *input.StartTime + } + if input.EndTime != nil { + room.EndTime = *input.EndTime + } + if input.NeedReservation != nil { + room.NeedReservation = *input.NeedReservation + if *input.NeedReservation && room.ReservationStatus == "not_required" { + room.ReservationStatus = "pending" + } + } + if input.GenderRule != nil { + room.GenderRule = strings.TrimSpace(*input.GenderRule) + } + if input.MemberLimit != nil { + value := int(*input.MemberLimit) + room.MemberLimit = &value + } + if input.Organization != nil { + room.Organization = utils.NormalizeWhitespace(*input.Organization) + } + if input.LevelDesc != nil { + room.LevelDesc = strings.TrimSpace(*input.LevelDesc) + } + if input.Description != nil { + room.Description = strings.TrimSpace(*input.Description) + } + + if !room.EndTime.After(room.StartTime) { + return nil, fmt.Errorf("end_time must be after start_time") + } + + if err := s.repo.Update(ctx, room); err != nil { + return nil, err + } + + return room, nil } func (s *RoomService) Close(ctx context.Context, roomID uint) (*models.Room, error) { - return nil, fmt.Errorf("room service Close not implemented") + currentUser, err := s.userService.GetCurrent(ctx) + if err != nil { + return nil, err + } + + room, err := s.repo.GetByID(ctx, roomID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("room not found") + } + return nil, err + } + + if room.OwnerID != currentUser.ID { + return nil, fmt.Errorf("only the owner can close the room") + } + + if room.Status == "cancelled" { + return nil, fmt.Errorf("room is already cancelled") + } + + room.Status = "cancelled" + if err := s.repo.Update(ctx, room); err != nil { + return nil, err + } + + return room, nil } func (s *RoomService) JoinByCode(ctx context.Context, input JoinRoomByCodeInput) (*JoinRoomOutput, error) { - return nil, fmt.Errorf("room service JoinByCode not implemented") + buddyCode := strings.TrimSpace(input.BuddyCode) + if buddyCode == "" { + return nil, fmt.Errorf("buddy_code is required") + } + + room, err := s.repo.GetByBuddyCode(ctx, buddyCode) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("room not found") + } + return nil, err + } + + return s.joinRoom(ctx, room) } func (s *RoomService) JoinDirectly(ctx context.Context, roomID uint) (*JoinRoomOutput, error) { - return nil, fmt.Errorf("room service JoinDirectly not implemented") + room, err := s.repo.GetByID(ctx, roomID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("room not found") + } + return nil, err + } + + return s.joinRoom(ctx, room) } func (s *RoomService) CreateJoinRequest(ctx context.Context, roomID uint, input CreateJoinRequestInput) (*models.JoinRequest, error) { - return nil, fmt.Errorf("room service CreateJoinRequest not implemented") + message := strings.TrimSpace(input.Message) + if message == "" { + return nil, fmt.Errorf("message is required") + } + + currentUser, err := s.userService.GetCurrent(ctx) + if err != nil { + return nil, err + } + + if _, err := s.repo.GetByID(ctx, roomID); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("room not found") + } + return nil, err + } + + if _, err := s.repo.GetMember(ctx, roomID, currentUser.ID); err == nil { + return nil, fmt.Errorf("already a member") + } else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + + if _, err := s.repo.GetPendingJoinRequestByRoomAndUser(ctx, roomID, currentUser.ID); err == nil { + return nil, fmt.Errorf("already have a pending join request") + } else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + + req := &models.JoinRequest{ + RoomID: roomID, + UserID: currentUser.ID, + Status: "pending", + Message: message, + } + if err := s.repo.CreateJoinRequest(ctx, req); err != nil { + return nil, err + } + + return req, nil } func (s *RoomService) ApproveJoinRequest(ctx context.Context, roomID uint, input ReviewJoinRequestInput) (*models.JoinRequest, error) { - return nil, fmt.Errorf("room service ApproveJoinRequest not implemented") + currentUser, err := s.userService.GetCurrent(ctx) + if err != nil { + return nil, err + } + + room, err := s.repo.GetByID(ctx, roomID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("room not found") + } + return nil, err + } + + if room.OwnerID != currentUser.ID { + return nil, fmt.Errorf("only the owner can review join requests") + } + + req, err := s.repo.GetJoinRequestByID(ctx, input.RequestID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("join request not found") + } + return nil, err + } + + if req.RoomID != roomID { + return nil, fmt.Errorf("join request does not belong to this room") + } + + if req.Status != "pending" { + return nil, fmt.Errorf("join request has already been reviewed") + } + + if room.Status != "recruiting" { + return nil, fmt.Errorf("room is not recruiting") + } + + if _, err := s.repo.GetMember(ctx, roomID, req.UserID); err == nil { + return nil, fmt.Errorf("user is already a member") + } else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + + if room.MemberLimit != nil { + count, err := s.repo.CountActiveMembers(ctx, roomID) + if err != nil { + return nil, err + } + if count >= int64(*room.MemberLimit) { + return nil, fmt.Errorf("room is full") + } + } + + now := time.Now() + req.Status = "approved" + req.ReviewedBy = ¤tUser.ID + req.ReviewedAt = &now + + if err := s.repo.CreateMember(ctx, &models.RoomMember{ + RoomID: roomID, + UserID: req.UserID, + Role: "member", + Status: "joined", + JoinedAt: &now, + CreatedAt: now, + UpdatedAt: now, + }); err != nil { + return nil, err + } + + if err := s.repo.UpdateJoinRequest(ctx, req); err != nil { + return nil, err + } + + return req, nil } func (s *RoomService) RejectJoinRequest(ctx context.Context, roomID uint, input ReviewJoinRequestInput) (*models.JoinRequest, error) { - return nil, fmt.Errorf("room service RejectJoinRequest not implemented") + currentUser, err := s.userService.GetCurrent(ctx) + if err != nil { + return nil, err + } + + room, err := s.repo.GetByID(ctx, roomID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("room not found") + } + return nil, err + } + + if room.OwnerID != currentUser.ID { + return nil, fmt.Errorf("only the owner can review join requests") + } + + req, err := s.repo.GetJoinRequestByID(ctx, input.RequestID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("join request not found") + } + return nil, err + } + + if req.RoomID != roomID { + return nil, fmt.Errorf("join request does not belong to this room") + } + + if req.Status != "pending" { + return nil, fmt.Errorf("join request has already been reviewed") + } + + now := time.Now() + req.Status = "rejected" + req.ReviewedBy = ¤tUser.ID + req.ReviewedAt = &now + + if err := s.repo.UpdateJoinRequest(ctx, req); err != nil { + return nil, err + } + + return req, nil } func (s *RoomService) InviteMember(ctx context.Context, roomID uint, input InviteMemberInput) (*models.RoomMember, error) { - return nil, fmt.Errorf("room service InviteMember not implemented") + currentUser, err := s.userService.GetCurrent(ctx) + if err != nil { + return nil, err + } + + room, err := s.repo.GetByID(ctx, roomID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("room not found") + } + return nil, err + } + + if room.OwnerID != currentUser.ID { + return nil, fmt.Errorf("only the owner can invite members") + } + + if room.Status != "recruiting" { + return nil, fmt.Errorf("room is not recruiting") + } + + if input.UserID == currentUser.ID { + return nil, fmt.Errorf("cannot invite yourself") + } + + targetUser, err := s.userService.GetByID(ctx, input.UserID) + if err != nil { + return nil, fmt.Errorf("user not found") + } + + if _, err := s.repo.GetMember(ctx, roomID, input.UserID); err == nil { + return nil, fmt.Errorf("user is already a member") + } else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + + if room.MemberLimit != nil { + count, err := s.repo.CountActiveMembers(ctx, roomID) + if err != nil { + return nil, err + } + if count >= int64(*room.MemberLimit) { + return nil, fmt.Errorf("room is full") + } + } + + now := time.Now() + member := &models.RoomMember{ + RoomID: roomID, + UserID: input.UserID, + Role: "member", + Status: "joined", + JoinedAt: &now, + CreatedAt: now, + UpdatedAt: now, + } + if err := s.repo.CreateMember(ctx, member); err != nil { + return nil, err + } + + member.User = *targetUser + return member, nil } func (s *RoomService) RemoveMember(ctx context.Context, roomID, userID uint) (*models.RoomMember, error) { - return nil, fmt.Errorf("room service RemoveMember not implemented") + currentUser, err := s.userService.GetCurrent(ctx) + if err != nil { + return nil, err + } + + room, err := s.repo.GetByID(ctx, roomID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("room not found") + } + return nil, err + } + + if room.OwnerID != currentUser.ID { + return nil, fmt.Errorf("only the owner can remove members") + } + + if room.Status != "recruiting" { + return nil, fmt.Errorf("room is not recruiting") + } + + member, err := s.repo.GetMember(ctx, roomID, userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("user is not a member of this room") + } + return nil, err + } + + if member.Role == "owner" { + return nil, fmt.Errorf("cannot remove the room owner") + } + + if err := s.repo.DeleteMember(ctx, roomID, userID); err != nil { + return nil, err + } + + return member, nil +} + +func (s *RoomService) ListMineCreated(ctx context.Context, page, pageSize int) (*ListRoomsOutput, error) { + currentUser, err := s.userService.GetCurrent(ctx) + if err != nil { + return nil, err + } + + if page < 1 { + page = 1 + } + if pageSize < 1 { + pageSize = 20 + } + + result, err := s.repo.ListRoomsByOwner(ctx, currentUser.ID, page, pageSize) + if err != nil { + return nil, err + } + + items := make([]RoomCardItem, 0, len(result.Items)) + for _, room := range result.Items { + count, countErr := s.repo.CountActiveMembers(ctx, room.ID) + if countErr != nil { + return nil, countErr + } + items = append(items, RoomCardItem{ + Room: room, + CurrentMemberCount: int32(count), + }) + } + + return &ListRoomsOutput{ + Page: int32(page), + PageSize: int32(pageSize), + Total: result.Total, + Items: items, + }, nil +} + +func (s *RoomService) ListMineJoined(ctx context.Context, page, pageSize int) (*ListRoomsOutput, error) { + currentUser, err := s.userService.GetCurrent(ctx) + if err != nil { + return nil, err + } + + if page < 1 { + page = 1 + } + if pageSize < 1 { + pageSize = 20 + } + + result, err := s.repo.ListRoomsJoinedByUser(ctx, currentUser.ID, page, pageSize) + if err != nil { + return nil, err + } + + items := make([]RoomCardItem, 0, len(result.Items)) + for _, room := range result.Items { + count, countErr := s.repo.CountActiveMembers(ctx, room.ID) + if countErr != nil { + return nil, countErr + } + items = append(items, RoomCardItem{ + Room: room, + CurrentMemberCount: int32(count), + }) + } + + return &ListRoomsOutput{ + Page: int32(page), + PageSize: int32(pageSize), + Total: result.Total, + Items: items, + }, nil +} + +func (s *RoomService) GetMyStats(ctx context.Context) (*UserStatsOutput, error) { + currentUser, err := s.userService.GetCurrent(ctx) + if err != nil { + return nil, err + } + + createdCount, err := s.repo.CountRoomsByOwner(ctx, currentUser.ID) + if err != nil { + return nil, err + } + + joinedCount, err := s.repo.CountJoinedRoomsByUser(ctx, currentUser.ID) + if err != nil { + return nil, err + } + + pendingCount, err := s.repo.CountPendingJoinRequestsByUser(ctx, currentUser.ID) + if err != nil { + return nil, err + } + + return &UserStatsOutput{ + CreatedRoomCount: createdCount, + JoinedRoomCount: joinedCount, + PendingJoinRequestCount: pendingCount, + }, nil +} + +func (s *RoomService) joinRoom(ctx context.Context, room *models.Room) (*JoinRoomOutput, error) { + currentUser, err := s.userService.GetCurrent(ctx) + if err != nil { + return nil, err + } + + if _, err := s.repo.GetMember(ctx, room.ID, currentUser.ID); err == nil { + status := "joined" + return &JoinRoomOutput{RoomID: room.ID, RoomPublicID: room.PublicID, JoinResult: "already_joined", MemberStatus: &status}, nil + } else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + + if room.Status != "recruiting" { + return nil, fmt.Errorf("room is not recruiting") + } + + if room.JoinMode == "approval" { + if _, err := s.repo.GetPendingJoinRequestByRoomAndUser(ctx, room.ID, currentUser.ID); err == nil { + return nil, fmt.Errorf("already have a pending join request") + } else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + pending := "pending" + request := &models.JoinRequest{ + RoomID: room.ID, + UserID: currentUser.ID, + Status: pending, + Message: "joined via direct join", + } + if err := s.repo.CreateJoinRequest(ctx, request); err != nil { + return nil, err + } + return &JoinRoomOutput{RoomID: room.ID, RoomPublicID: room.PublicID, JoinResult: "request_created", RequestStatus: &pending}, nil + } + + if room.MemberLimit != nil { + count, err := s.repo.CountActiveMembers(ctx, room.ID) + if err != nil { + return nil, err + } + if count >= int64(*room.MemberLimit) { + return nil, fmt.Errorf("room is full") + } + } + + now := time.Now() + if err := s.repo.CreateMember(ctx, &models.RoomMember{ + RoomID: room.ID, + UserID: currentUser.ID, + Role: "member", + Status: "joined", + JoinedAt: &now, + CreatedAt: now, + UpdatedAt: now, + }); err != nil { + return nil, err + } + + joinedStatus := "joined" + return &JoinRoomOutput{RoomID: room.ID, RoomPublicID: room.PublicID, JoinResult: "joined", MemberStatus: &joinedStatus}, nil +} + +func generateInviteCode() string { + return fmt.Sprintf("ROOM%06d", time.Now().UnixNano()%1000000) +} + +func isBuddyCodeSport(sportType string) bool { + switch strings.ToLower(strings.TrimSpace(sportType)) { + case "tennis", "badminton": + return true + default: + return false + } } diff --git a/internal/service/user_service.go b/internal/service/user_service.go index a748f65..12b9974 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -3,9 +3,12 @@ package service import ( "context" "fmt" + "strings" "github.com/QSCTech/SRTP-Backend/internal/repository" "github.com/QSCTech/SRTP-Backend/models" + "github.com/QSCTech/SRTP-Backend/pkg/utils" + "gorm.io/gorm" ) type UserService struct { @@ -24,21 +27,76 @@ func NewUserService(repo *repository.UserRepository) *UserService { } func (s *UserService) Create(ctx context.Context, authUID string) (*models.User, error) { - return nil, fmt.Errorf("user service Create not implemented") + authUID = utils.NormalizeWhitespace(authUID) + if strings.TrimSpace(authUID) == "" { + return nil, fmt.Errorf("auth_uid is required") + } + user := &models.User{AuthUID: authUID} + if err := s.repo.Create(ctx, user); err != nil { + return nil, err + } + return user, nil } func (s *UserService) GetByID(ctx context.Context, id uint) (*models.User, error) { - return nil, fmt.Errorf("user service GetByID not implemented") + user, err := s.repo.GetByID(ctx, id) + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("user not found") + } + return nil, err + } + return user, nil } func (s *UserService) GetCurrent(ctx context.Context) (*models.User, error) { - return nil, fmt.Errorf("user service GetCurrent not implemented") + user, err := s.repo.GetFirst(ctx) + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("user not found") + } + return nil, err + } + return user, nil } func (s *UserService) UpdateCurrentProfile(ctx context.Context, input UpdateProfileInput) (*models.User, error) { - return nil, fmt.Errorf("user service UpdateCurrentProfile not implemented") + user, err := s.GetCurrent(ctx) + if err != nil { + return nil, err + } + if input.Nickname != nil { + user.Nickname = utils.NormalizeWhitespace(*input.Nickname) + } + if input.AvatarURL != nil { + user.AvatarURL = strings.TrimSpace(*input.AvatarURL) + } + if input.Gender != nil { + user.Gender = strings.TrimSpace(*input.Gender) + } + if input.Bio != nil { + user.Bio = utils.NormalizeWhitespace(*input.Bio) + } + if err := s.repo.Update(ctx, user); err != nil { + return nil, err + } + return user, nil } func (s *UserService) LoginOrCreate(ctx context.Context, authUID, openID string) (*models.User, error) { - return nil, fmt.Errorf("user service LoginOrCreate not implemented") + user, err := s.repo.GetByAuthUID(ctx, authUID) + if err == nil { + return user, nil + } + if err != gorm.ErrRecordNotFound { + return nil, err + } + user = &models.User{ + AuthUID: authUID, + OpenID: openID, + } + if err := s.repo.Create(ctx, user); err != nil { + return nil, err + } + return user, nil } From 62218b29d4d3eb03e5b6315ea290d77dac5c9948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E9=99=88=E9=91=AB?= <3240103237@zju.edu.cn> Date: Sun, 24 May 2026 21:44:42 +0800 Subject: [PATCH 2/3] feat: implement room creation and management (Part 5), fixed some problems in review --- internal/api/router.go | 8 +++ internal/repository/room_repository.go | 13 ++++ internal/service/room_service.go | 86 +++++++++++++++++++++++--- internal/service/user_service.go | 22 +++++++ 4 files changed, 122 insertions(+), 7 deletions(-) diff --git a/internal/api/router.go b/internal/api/router.go index f9c6b44..5861cb6 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -1,6 +1,7 @@ package api import ( + "context" "database/sql" "github.com/QSCTech/SRTP-Backend/internal/api/gen" @@ -13,6 +14,13 @@ import ( func NewRouter(log *zap.Logger, db *sql.DB, userService *service.UserService, roomService *service.RoomService, reservationService *service.ReservationService) *gin.Engine { engine := gin.New() engine.Use(middleware.Zap(log), middleware.Recovery(log)) + engine.Use(func(c *gin.Context) { + if mockUserID := c.GetHeader("X-Mock-User-ID"); mockUserID != "" { + ctx := context.WithValue(c.Request.Context(), service.MockUserIDKey, mockUserID) + c.Request = c.Request.WithContext(ctx) + } + c.Next() + }) handler := NewHandler(db, userService, roomService, reservationService) gen.RegisterHandlers(engine, handler) diff --git a/internal/repository/room_repository.go b/internal/repository/room_repository.go index 9f9d59d..a47d22d 100644 --- a/internal/repository/room_repository.go +++ b/internal/repository/room_repository.go @@ -32,6 +32,19 @@ func (r *RoomRepository) Create(ctx context.Context, room *models.Room) error { return r.db.WithContext(ctx).Create(room).Error } +func (r *RoomRepository) CreateRoomWithOwner(ctx context.Context, room *models.Room, ownerMember *models.RoomMember) error { + return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if err := tx.Create(room).Error; err != nil { + return err + } + ownerMember.RoomID = room.ID + if err := tx.Create(ownerMember).Error; err != nil { + return err + } + return nil + }) +} + func (r *RoomRepository) Update(ctx context.Context, room *models.Room) error { return r.db.WithContext(ctx).Save(room).Error } diff --git a/internal/service/room_service.go b/internal/service/room_service.go index 85f0449..e2d2a5b 100644 --- a/internal/service/room_service.go +++ b/internal/service/room_service.go @@ -66,6 +66,15 @@ type UpdateRoomInput struct { Description *string } +// ============================================================================= +// Part 5 (5组) - 房间创建与房间管理:Create, Update, Close +// Part 6 (6组) - 成员加入与房主审批(临时补充实现,待6组替换): JoinByCode, JoinDirectly, +// CreateJoinRequest, ApproveJoinRequest, RejectJoinRequest, +// InviteMember, RemoveMember, joinRoom +// Part 3 (3组) - 登录与用户资料(临时补充实现,待3组替换): ListMineCreated, +// ListMineJoined, GetMyStats +// ============================================================================= + type JoinRoomByCodeInput struct { BuddyCode string } @@ -223,6 +232,12 @@ func (s *RoomService) Create(ctx context.Context, input CreateRoomInput) (*model if strings.TrimSpace(input.JoinMode) == "" { return nil, fmt.Errorf("join_mode is required") } + if !isValidVisibility(input.Visibility) { + return nil, fmt.Errorf("visibility must be 'public' or 'private'") + } + if !isValidJoinMode(input.JoinMode) { + return nil, fmt.Errorf("join_mode must be 'direct', 'approval', or 'invite_only'") + } if input.StartTime.IsZero() { return nil, fmt.Errorf("start_time is required") } @@ -274,13 +289,12 @@ func (s *RoomService) Create(ctx context.Context, input CreateRoomInput) (*model room.ReservationStatus = "pending" } - if err := s.repo.Create(ctx, room); err != nil { - return nil, err + if room.MemberLimit != nil && *room.MemberLimit <= 0 { + return nil, fmt.Errorf("member_limit must be greater than 0") } now := time.Now() - if err := s.repo.CreateMember(ctx, &models.RoomMember{ - RoomID: room.ID, + if err := s.repo.CreateRoomWithOwner(ctx, room, &models.RoomMember{ UserID: currentUser.ID, Role: "owner", Status: "joined", @@ -290,6 +304,7 @@ func (s *RoomService) Create(ctx context.Context, input CreateRoomInput) (*model }); err != nil { return nil, err } + tryMarkFull(ctx, s.repo, room) room.Owner = *currentUser return room, nil @@ -325,9 +340,15 @@ func (s *RoomService) Update(ctx context.Context, roomID uint, input UpdateRoomI room.Name = name } if input.Visibility != nil { + if !isValidVisibility(*input.Visibility) { + return nil, fmt.Errorf("visibility must be 'public' or 'private'") + } room.Visibility = strings.TrimSpace(*input.Visibility) } if input.JoinMode != nil { + if !isValidJoinMode(*input.JoinMode) { + return nil, fmt.Errorf("join_mode must be 'direct', 'approval', or 'invite_only'") + } room.JoinMode = strings.TrimSpace(*input.JoinMode) } if input.StartTime != nil { @@ -340,6 +361,8 @@ func (s *RoomService) Update(ctx context.Context, roomID uint, input UpdateRoomI room.NeedReservation = *input.NeedReservation if *input.NeedReservation && room.ReservationStatus == "not_required" { room.ReservationStatus = "pending" + } else if !*input.NeedReservation { + room.ReservationStatus = "not_required" } } if input.GenderRule != nil { @@ -347,6 +370,16 @@ func (s *RoomService) Update(ctx context.Context, roomID uint, input UpdateRoomI } if input.MemberLimit != nil { value := int(*input.MemberLimit) + if value <= 0 { + return nil, fmt.Errorf("member_limit must be greater than 0") + } + count, err := s.repo.CountActiveMembers(ctx, roomID) + if err != nil { + return nil, err + } + if value < int(count) { + return nil, fmt.Errorf("member_limit cannot be less than current member count (%d)", count) + } room.MemberLimit = &value } if input.Organization != nil { @@ -414,7 +447,7 @@ func (s *RoomService) JoinByCode(ctx context.Context, input JoinRoomByCodeInput) return nil, err } - return s.joinRoom(ctx, room) + return s.joinRoom(ctx, room, true) } func (s *RoomService) JoinDirectly(ctx context.Context, roomID uint) (*JoinRoomOutput, error) { @@ -426,7 +459,7 @@ func (s *RoomService) JoinDirectly(ctx context.Context, roomID uint) (*JoinRoomO return nil, err } - return s.joinRoom(ctx, room) + return s.joinRoom(ctx, room, false) } func (s *RoomService) CreateJoinRequest(ctx context.Context, roomID uint, input CreateJoinRequestInput) (*models.JoinRequest, error) { @@ -542,6 +575,7 @@ func (s *RoomService) ApproveJoinRequest(ctx context.Context, roomID uint, input }); err != nil { return nil, err } + tryMarkFull(ctx, s.repo, room) if err := s.repo.UpdateJoinRequest(ctx, req); err != nil { return nil, err @@ -656,6 +690,7 @@ func (s *RoomService) InviteMember(ctx context.Context, roomID uint, input Invit if err := s.repo.CreateMember(ctx, member); err != nil { return nil, err } + tryMarkFull(ctx, s.repo, room) member.User = *targetUser return member, nil @@ -806,7 +841,7 @@ func (s *RoomService) GetMyStats(ctx context.Context) (*UserStatsOutput, error) }, nil } -func (s *RoomService) joinRoom(ctx context.Context, room *models.Room) (*JoinRoomOutput, error) { +func (s *RoomService) joinRoom(ctx context.Context, room *models.Room, bypassJoinMode bool) (*JoinRoomOutput, error) { currentUser, err := s.userService.GetCurrent(ctx) if err != nil { return nil, err @@ -823,6 +858,10 @@ func (s *RoomService) joinRoom(ctx context.Context, room *models.Room) (*JoinRoo return nil, fmt.Errorf("room is not recruiting") } + if room.JoinMode == "invite_only" && !bypassJoinMode { + return nil, fmt.Errorf("room is invite-only, please use invite code") + } + if room.JoinMode == "approval" { if _, err := s.repo.GetPendingJoinRequestByRoomAndUser(ctx, room.ID, currentUser.ID); err == nil { return nil, fmt.Errorf("already have a pending join request") @@ -866,6 +905,7 @@ func (s *RoomService) joinRoom(ctx context.Context, room *models.Room) (*JoinRoo } joinedStatus := "joined" + tryMarkFull(ctx, s.repo, room) return &JoinRoomOutput{RoomID: room.ID, RoomPublicID: room.PublicID, JoinResult: "joined", MemberStatus: &joinedStatus}, nil } @@ -873,6 +913,38 @@ func generateInviteCode() string { return fmt.Sprintf("ROOM%06d", time.Now().UnixNano()%1000000) } +func tryMarkFull(ctx context.Context, repo *repository.RoomRepository, room *models.Room) { + if room.MemberLimit == nil { + return + } + count, err := repo.CountActiveMembers(ctx, room.ID) + if err != nil { + return + } + if count >= int64(*room.MemberLimit) && room.Status == "recruiting" { + room.Status = "full" + _ = repo.Update(ctx, room) + } +} + +func isValidVisibility(v string) bool { + switch strings.TrimSpace(v) { + case "public", "private": + return true + default: + return false + } +} + +func isValidJoinMode(m string) bool { + switch strings.TrimSpace(m) { + case "direct", "approval", "invite_only": + return true + default: + return false + } +} + func isBuddyCodeSport(sportType string) bool { switch strings.ToLower(strings.TrimSpace(sportType)) { case "tennis", "badminton": diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 12b9974..e46bf85 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -3,6 +3,7 @@ package service import ( "context" "fmt" + "strconv" "strings" "github.com/QSCTech/SRTP-Backend/internal/repository" @@ -11,6 +12,13 @@ import ( "gorm.io/gorm" ) +type contextKey string + +const MockUserIDKey contextKey = "mock_user_id" + +// Part 3 (3组) - 登录与用户资料(临时补充实现,待3组替换): +// Create, GetByID, GetCurrent, UpdateCurrentProfile, LoginOrCreate + type UserService struct { repo *repository.UserRepository } @@ -50,6 +58,20 @@ func (s *UserService) GetByID(ctx context.Context, id uint) (*models.User, error } func (s *UserService) GetCurrent(ctx context.Context) (*models.User, error) { + if mockID, ok := ctx.Value(MockUserIDKey).(string); ok && mockID != "" { + id, err := strconv.ParseUint(mockID, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid X-Mock-User-ID: %s", mockID) + } + user, err := s.repo.GetByID(ctx, uint(id)) + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("mock user %d not found", id) + } + return nil, err + } + return user, nil + } user, err := s.repo.GetFirst(ctx) if err != nil { if err == gorm.ErrRecordNotFound { From 2e9c9d70f05929bac4e02efed531ce84cc2f8457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E9=99=88=E9=91=AB?= <3240103237@zju.edu.cn> Date: Wed, 3 Jun 2026 14:53:20 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20align=20with=20Group=203=20auth,=20f?= =?UTF-8?q?ull-room=20update,=20secure=20invite=20code=20-=20Add=20middlew?= =?UTF-8?q?are/auth.go=20(RequireAuth/CurrentUser/AuthByRoute)=20-=20Repla?= =?UTF-8?q?ce=20X-Mock-User-ID=20with=20AuthByRoute=20in=20router.go=20-?= =?UTF-8?q?=20Sync=20user=5Fservice.go=20with=20upstream=20Group=203=20imp?= =?UTF-8?q?lementation=20-=20Add=20UpdateProfileWithAudit=20to=20user=5Fre?= =?UTF-8?q?pository=20-=20Allow=20owner=20to=20update=20room=20in=20'full'?= =?UTF-8?q?=20status=20-=20Auto-transition=20full=E2=86=92recruiting=20whe?= =?UTF-8?q?n=20capacity=20expanded=20-=20Use=20crypto/rand=20for=20invite?= =?UTF-8?q?=5Fcode=20generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/api/router.go | 12 +--- internal/middleware/auth.go | 84 ++++++++++++++++++++++ internal/repository/user_repository.go | 13 ++++ internal/service/room_service.go | 17 ++++- internal/service/user_service.go | 99 ++++++++++++++------------ 5 files changed, 168 insertions(+), 57 deletions(-) create mode 100644 internal/middleware/auth.go diff --git a/internal/api/router.go b/internal/api/router.go index 5861cb6..8039d22 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -1,7 +1,6 @@ package api import ( - "context" "database/sql" "github.com/QSCTech/SRTP-Backend/internal/api/gen" @@ -14,16 +13,11 @@ import ( func NewRouter(log *zap.Logger, db *sql.DB, userService *service.UserService, roomService *service.RoomService, reservationService *service.ReservationService) *gin.Engine { engine := gin.New() engine.Use(middleware.Zap(log), middleware.Recovery(log)) - engine.Use(func(c *gin.Context) { - if mockUserID := c.GetHeader("X-Mock-User-ID"); mockUserID != "" { - ctx := context.WithValue(c.Request.Context(), service.MockUserIDKey, mockUserID) - c.Request = c.Request.WithContext(ctx) - } - c.Next() - }) handler := NewHandler(db, userService, roomService, reservationService) - gen.RegisterHandlers(engine, handler) + gen.RegisterHandlersWithOptions(engine, handler, gen.GinServerOptions{ + Middlewares: []gen.MiddlewareFunc{gen.MiddlewareFunc(middleware.AuthByRoute())}, + }) return engine } diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go new file mode 100644 index 0000000..336f2f9 --- /dev/null +++ b/internal/middleware/auth.go @@ -0,0 +1,84 @@ +package middleware + +import ( + "context" + "net/http" + "strings" + + "github.com/QSCTech/SRTP-Backend/pkg/response" + "github.com/gin-gonic/gin" +) + +type contextKey string + +const AuthUIDKey contextKey = "auth_uid" + +const authUIDHeader = "X-Auth-UID" + +func RequireAuth() gin.HandlerFunc { + return func(c *gin.Context) { + authUID := extractAuthUID(c) + if authUID == "" { + response.Error(c, http.StatusUnauthorized, "unauthorized") + c.Abort() + return + } + setAuthUID(c, authUID) + c.Next() + } +} + +func CurrentUser() gin.HandlerFunc { + return func(c *gin.Context) { + setAuthUID(c, extractAuthUID(c)) + c.Next() + } +} + +func AuthByRoute() gin.HandlerFunc { + requireAuth := RequireAuth() + currentUser := CurrentUser() + + return func(c *gin.Context) { + switch { + case routeNeedsAuth(c.Request.Method, c.FullPath()): + requireAuth(c) + case routeAllowsCurrentUser(c.Request.Method, c.FullPath()): + currentUser(c) + default: + c.Next() + } + } +} + +func routeNeedsAuth(method, path string) bool { + switch { + case method == http.MethodPost && path == "/auth/logout": + return true + case strings.HasPrefix(path, "/me"): + return true + case method == http.MethodPost && path == "/rooms": + return true + case method == http.MethodPost && path == "/rooms/join-by-code": + return true + case path == "/rooms/:roomId" && method == http.MethodPut: + return true + case strings.HasPrefix(path, "/rooms/:roomId/") && method == http.MethodPost: + return true + default: + return false + } +} + +func routeAllowsCurrentUser(method, path string) bool { + return method == http.MethodGet && path == "/rooms/:roomId" +} + +func extractAuthUID(c *gin.Context) string { + return strings.TrimSpace(c.GetHeader(authUIDHeader)) +} + +func setAuthUID(c *gin.Context, authUID string) { + ctx := context.WithValue(c.Request.Context(), AuthUIDKey, authUID) + c.Request = c.Request.WithContext(ctx) +} diff --git a/internal/repository/user_repository.go b/internal/repository/user_repository.go index c028f48..34f0250 100644 --- a/internal/repository/user_repository.go +++ b/internal/repository/user_repository.go @@ -54,3 +54,16 @@ func (r *UserRepository) GetFirst(ctx context.Context) (*models.User, error) { func (r *UserRepository) Update(ctx context.Context, user *models.User) error { return r.db.WithContext(ctx).Save(user).Error } + +func (r *UserRepository) CreateProfileAudit(ctx context.Context, audit *models.UserProfileAudit) error { + return r.db.WithContext(ctx).Create(audit).Error +} + +func (r *UserRepository) UpdateProfileWithAudit(ctx context.Context, user *models.User, audit *models.UserProfileAudit) error { + return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if err := tx.Save(user).Error; err != nil { + return err + } + return tx.Create(audit).Error + }) +} diff --git a/internal/service/room_service.go b/internal/service/room_service.go index e2d2a5b..324d0de 100644 --- a/internal/service/room_service.go +++ b/internal/service/room_service.go @@ -2,7 +2,9 @@ package service import ( "context" + "crypto/rand" "fmt" + "math/big" "strings" "time" @@ -328,7 +330,7 @@ func (s *RoomService) Update(ctx context.Context, roomID uint, input UpdateRoomI return nil, fmt.Errorf("only the owner can update the room") } - if room.Status != "recruiting" { + if room.Status != "recruiting" && room.Status != "full" { return nil, fmt.Errorf("room is not active") } @@ -381,6 +383,10 @@ func (s *RoomService) Update(ctx context.Context, roomID uint, input UpdateRoomI return nil, fmt.Errorf("member_limit cannot be less than current member count (%d)", count) } room.MemberLimit = &value + // 扩大容量时若房间已满,恢复为招募中 + if room.Status == "full" && value > int(count) { + room.Status = "recruiting" + } } if input.Organization != nil { room.Organization = utils.NormalizeWhitespace(*input.Organization) @@ -909,8 +915,15 @@ func (s *RoomService) joinRoom(ctx context.Context, room *models.Room, bypassJoi return &JoinRoomOutput{RoomID: room.ID, RoomPublicID: room.PublicID, JoinResult: "joined", MemberStatus: &joinedStatus}, nil } +var inviteCodeChars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + func generateInviteCode() string { - return fmt.Sprintf("ROOM%06d", time.Now().UnixNano()%1000000) + code := make([]byte, 8) + for i := range code { + n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(inviteCodeChars)))) + code[i] = inviteCodeChars[n.Int64()] + } + return string(code) } func tryMarkFull(ctx context.Context, repo *repository.RoomRepository, room *models.Room) { diff --git a/internal/service/user_service.go b/internal/service/user_service.go index e46bf85..140cabc 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -2,22 +2,20 @@ package service import ( "context" + "errors" "fmt" - "strconv" "strings" + "github.com/QSCTech/SRTP-Backend/internal/middleware" "github.com/QSCTech/SRTP-Backend/internal/repository" "github.com/QSCTech/SRTP-Backend/models" - "github.com/QSCTech/SRTP-Backend/pkg/utils" "gorm.io/gorm" ) -type contextKey string - -const MockUserIDKey contextKey = "mock_user_id" - -// Part 3 (3组) - 登录与用户资料(临时补充实现,待3组替换): -// Create, GetByID, GetCurrent, UpdateCurrentProfile, LoginOrCreate +var ( + ErrUnauthorized = errors.New("unauthorized") + ErrUserNotFound = errors.New("user not found") +) type UserService struct { repo *repository.UserRepository @@ -35,11 +33,17 @@ func NewUserService(repo *repository.UserRepository) *UserService { } func (s *UserService) Create(ctx context.Context, authUID string) (*models.User, error) { - authUID = utils.NormalizeWhitespace(authUID) - if strings.TrimSpace(authUID) == "" { - return nil, fmt.Errorf("auth_uid is required") + authUID = strings.TrimSpace(authUID) + if authUID == "" { + return nil, fmt.Errorf("auth uid is required") } - user := &models.User{AuthUID: authUID} + if _, err := s.repo.GetByAuthUID(ctx, authUID); err == nil { + return nil, fmt.Errorf("auth uid already exists") + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + + user := &models.User{AuthUID: authUID, ProfileStatus: "pending"} if err := s.repo.Create(ctx, user); err != nil { return nil, err } @@ -47,35 +51,19 @@ func (s *UserService) Create(ctx context.Context, authUID string) (*models.User, } func (s *UserService) GetByID(ctx context.Context, id uint) (*models.User, error) { - user, err := s.repo.GetByID(ctx, id) - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, fmt.Errorf("user not found") - } - return nil, err - } - return user, nil + return s.repo.GetByID(ctx, id) } func (s *UserService) GetCurrent(ctx context.Context) (*models.User, error) { - if mockID, ok := ctx.Value(MockUserIDKey).(string); ok && mockID != "" { - id, err := strconv.ParseUint(mockID, 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid X-Mock-User-ID: %s", mockID) - } - user, err := s.repo.GetByID(ctx, uint(id)) - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, fmt.Errorf("mock user %d not found", id) - } - return nil, err - } - return user, nil + authUID, _ := ctx.Value(middleware.AuthUIDKey).(string) + authUID = strings.TrimSpace(authUID) + if authUID == "" { + return nil, ErrUnauthorized } - user, err := s.repo.GetFirst(ctx) + user, err := s.repo.GetByAuthUID(ctx, authUID) if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, fmt.Errorf("user not found") + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrUserNotFound } return nil, err } @@ -87,36 +75,55 @@ func (s *UserService) UpdateCurrentProfile(ctx context.Context, input UpdateProf if err != nil { return nil, err } + if input.Nickname != nil { - user.Nickname = utils.NormalizeWhitespace(*input.Nickname) + user.Nickname = *input.Nickname } if input.AvatarURL != nil { - user.AvatarURL = strings.TrimSpace(*input.AvatarURL) + user.AvatarURL = *input.AvatarURL } if input.Gender != nil { - user.Gender = strings.TrimSpace(*input.Gender) + user.Gender = *input.Gender } if input.Bio != nil { - user.Bio = utils.NormalizeWhitespace(*input.Bio) + user.Bio = *input.Bio + } + user.ProfileStatus = "pending_review" + + audit := &models.UserProfileAudit{ + UserID: user.ID, + SubmittedNickname: user.Nickname, + SubmittedBio: user.Bio, + Status: "pending", } - if err := s.repo.Update(ctx, user); err != nil { + if err := s.repo.UpdateProfileWithAudit(ctx, user, audit); err != nil { return nil, err } return user, nil } func (s *UserService) LoginOrCreate(ctx context.Context, authUID, openID string) (*models.User, error) { + authUID = strings.TrimSpace(authUID) + openID = strings.TrimSpace(openID) + if authUID == "" { + return nil, fmt.Errorf("auth uid is required") + } + user, err := s.repo.GetByAuthUID(ctx, authUID) if err == nil { + if openID != "" && user.OpenID != openID { + user.OpenID = openID + if err := s.repo.Update(ctx, user); err != nil { + return nil, err + } + } return user, nil } - if err != gorm.ErrRecordNotFound { + if !errors.Is(err, gorm.ErrRecordNotFound) { return nil, err } - user = &models.User{ - AuthUID: authUID, - OpenID: openID, - } + + user = &models.User{AuthUID: authUID, OpenID: openID, ProfileStatus: "pending"} if err := s.repo.Create(ctx, user); err != nil { return nil, err }