package repository import ( "backend/pkg/chat/domain/chat" "backend/pkg/chat/domain/entity" "backend/pkg/chat/domain/repository" "backend/pkg/library/cassandra" "context" "fmt" "time" "github.com/gocql/gocql" ) type roomRepository struct { roomRepo cassandra.Repository[entity.Room] memberRepo cassandra.Repository[entity.RoomMember] userRoomRepo cassandra.Repository[entity.UserRoom] db *cassandra.DB keyspace string } // RoomRepositoryParam 創建 RoomRepository 所需的參數 type RoomRepositoryParam struct { DB *cassandra.DB Keyspace string } // MustRoomRepository 創建 RoomRepository(如果失敗會 panic) func MustRoomRepository(param RoomRepositoryParam) repository.RoomRepository { repo, err := NewRoomRepository(param.DB, param.Keyspace) if err != nil { panic(fmt.Sprintf("failed to create room repository: %v", err)) } return repo } // NewRoomRepository 創建新的聊天室 Repository func NewRoomRepository(db *cassandra.DB, keyspace string) (repository.RoomRepository, error) { roomRepo, err := cassandra.NewRepository[entity.Room](db, keyspace) if err != nil { return nil, err } memberRepo, err := cassandra.NewRepository[entity.RoomMember](db, keyspace) if err != nil { return nil, err } userRoomRepo, err := cassandra.NewRepository[entity.UserRoom](db, keyspace) if err != nil { return nil, err } return &roomRepository{ roomRepo: roomRepo, memberRepo: memberRepo, userRoomRepo: userRoomRepo, db: db, keyspace: keyspace, }, nil } // ==================== Room Interface 實作 ==================== func (r *roomRepository) Create(ctx context.Context, room *entity.Room) error { now := time.Now().UTC().UnixNano() if room.CreatedAt == 0 { room.CreatedAt = now } if room.UpdatedAt == 0 { room.UpdatedAt = now } room.RoomID = gocql.TimeUUID() return r.roomRepo.Insert(ctx, *room) } func (r *roomRepository) RoomGet(ctx context.Context, roomID string) (*entity.Room, error) { uuid, err := gocql.ParseUUID(roomID) if err != nil { return nil, err } room, err := r.roomRepo.Get(ctx, entity.Room{ RoomID: uuid, }) if err != nil { if cassandra.IsNotFound(err) { return nil, cassandra.ErrNotFound } return nil, err } return &room, nil } func (r *roomRepository) RoomUpdate(ctx context.Context, room *entity.Room) error { update := entity.Room{} now := time.Now().UTC().UnixNano() get, err := r.RoomGet(ctx, room.RoomID.String()) if err != nil { return err } update.CreatedAt = get.CreatedAt update.UpdatedAt = now update.Status = room.Status update.Name = room.Name update.RoomID = room.RoomID if err = r.roomRepo.Update(ctx, update); err != nil { return err } return nil } func (r *roomRepository) RoomDelete(ctx context.Context, roomID string) error { uuid, err := gocql.ParseUUID(roomID) if err != nil { return err } // 使用原生 CQL 語句來刪除,避免 cassandra library Delete 方法的 struct 綁定問題 e := entity.Room{} stmt := fmt.Sprintf("DELETE FROM %s.%s WHERE room_id = ?", r.keyspace, e.TableName()) return r.db.GetSession().Query(stmt, nil). Bind(uuid). WithContext(ctx). ExecRelease() } func (r *roomRepository) RoomList(ctx context.Context, param repository.ListRoomsReq) ([]entity.Room, error) { query := r.roomRepo.Query() if param.Status != "" { query = query.Where(cassandra.Eq("status", param.Status)).AllowFiltering() } if param.PageSize > 0 { query = query.Limit(param.PageSize) } if param.LastID != "" { lastUUID, err := gocql.ParseUUID(param.LastID) if err != nil { return nil, err } query = query.Where(cassandra.Lt("room_id", lastUUID)) } var rooms []entity.Room if err := query.Scan(ctx, &rooms); err != nil { return nil, err } return rooms, nil } func (r *roomRepository) RoomCount(ctx context.Context, param repository.CountRoomsReq) (int64, error) { query := r.roomRepo.Query() if param.Status != "" { query = query.Where(cassandra.Eq("status", param.Status)).AllowFiltering() } return query.Count(ctx) } func (r *roomRepository) RoomExists(ctx context.Context, roomID string) (bool, error) { uuid, err := gocql.ParseUUID(roomID) if err != nil { return false, err } _, err = r.roomRepo.Get(ctx, entity.Room{ RoomID: uuid, }) if err != nil { if cassandra.IsNotFound(err) { return false, nil } return false, err } return true, nil } func (r *roomRepository) RoomGetByID(ctx context.Context, roomIDs []string) ([]entity.Room, error) { if len(roomIDs) == 0 { return []entity.Room{}, nil } // 將字串 ID 轉換為 UUID uuids := make([]any, 0, len(roomIDs)) for _, id := range roomIDs { uuid, err := gocql.ParseUUID(id) if err != nil { return nil, err } uuids = append(uuids, uuid) } var rooms []entity.Room err := r.roomRepo.Query(). Where(cassandra.In("room_id", uuids)). Scan(ctx, &rooms) if err != nil { return nil, err } return rooms, nil } // ==================== Member Interface 實作 ==================== func (r *roomRepository) Insert(ctx context.Context, member *entity.RoomMember) error { now := time.Now().UTC().UnixNano() if member.JoinedAt == 0 { member.JoinedAt = now } if member.Role == "" { member.Role = chat.RoomRoleMember.String() } // 同時插入到 user_room 表(反向查詢表) userRoom := entity.UserRoom{ UID: member.UID, RoomID: member.RoomID, JoinedAt: member.JoinedAt, } if err := r.memberRepo.Insert(ctx, *member); err != nil { return err } if err := r.userRoomRepo.Insert(ctx, userRoom); err != nil { return err } return nil } func (r *roomRepository) Get(ctx context.Context, roomID, uid string) (*entity.RoomMember, error) { uuid, err := gocql.ParseUUID(roomID) if err != nil { return nil, err } member, err := r.memberRepo.Get(ctx, entity.RoomMember{ RoomID: uuid, UID: uid, }) if err != nil { if cassandra.IsNotFound(err) { return nil, cassandra.ErrNotFound } return nil, err } return &member, nil } func (r *roomRepository) AllMembers(ctx context.Context, roomID string) ([]entity.RoomMember, error) { uuid, err := gocql.ParseUUID(roomID) if err != nil { return nil, err } var members []entity.RoomMember err = r.memberRepo.Query(). Where(cassandra.Eq("room_id", uuid)). Scan(ctx, &members) if err != nil { return nil, err } return members, nil } func (r *roomRepository) UpdateRole(ctx context.Context, member *entity.RoomMember) error { get, err := r.Get(ctx, member.RoomID.String(), member.UID) if err != nil { return err } update := entity.RoomMember{ RoomID: member.RoomID, UID: member.UID, Role: member.Role, JoinedAt: get.JoinedAt, } return r.memberRepo.Update(ctx, update) } func (r *roomRepository) DeleteMember(ctx context.Context, roomID, uid string) error { uuid, err := gocql.ParseUUID(roomID) if err != nil { return err } // 同時從兩個表中刪除 if err := r.memberRepo.Delete(ctx, entity.RoomMember{ RoomID: uuid, UID: uid, }); err != nil { return err } if err := r.userRoomRepo.Delete(ctx, entity.UserRoom{ UID: uid, RoomID: uuid, }); err != nil { return err } return nil } func (r *roomRepository) DeleteRoom(ctx context.Context, roomID string) error { uuid, err := gocql.ParseUUID(roomID) if err != nil { return err } // 先查詢所有成員(必須在刪除之前查詢) members, err := r.AllMembers(ctx, roomID) if err != nil { return err } // 刪除 user_room 表中的所有關聯 for _, member := range members { if err := r.userRoomRepo.Delete(ctx, entity.UserRoom{ UID: member.UID, RoomID: uuid, }); err != nil { return err } } // 最後刪除 room_member 表中的所有成員 e := entity.RoomMember{} stmt := fmt.Sprintf("DELETE FROM %s.%s WHERE room_id = ?", r.keyspace, e.TableName()) if err := r.db.GetSession().Query(stmt, nil). Bind(uuid). WithContext(ctx). WithTimestamp(time.Now().UnixNano() / 1e3). ExecRelease(); err != nil { return err } return nil } func (r *roomRepository) Count(ctx context.Context, roomID string) (int64, error) { uuid, err := gocql.ParseUUID(roomID) if err != nil { return 0, err } return r.memberRepo.Query(). Where(cassandra.Eq("room_id", uuid)). Count(ctx) } // ==================== User Interface 實作 ==================== func (r *roomRepository) GetUserRooms(ctx context.Context, uid string) ([]entity.UserRoom, error) { var userRooms []entity.UserRoom err := r.userRoomRepo.Query(). Where(cassandra.Eq("uid", uid)). Scan(ctx, &userRooms) if err != nil { return nil, err } return userRooms, nil } func (r *roomRepository) CountUserRooms(ctx context.Context, uid string) (int64, error) { return r.userRoomRepo.Query(). Where(cassandra.Eq("uid", uid)). Count(ctx) } func (r *roomRepository) IsUserInRoom(ctx context.Context, uid, roomID string) (bool, error) { uuid, err := gocql.ParseUUID(roomID) if err != nil { return false, err } _, err = r.userRoomRepo.Get(ctx, entity.UserRoom{ UID: uid, RoomID: uuid, }) if err != nil { if cassandra.IsNotFound(err) { return false, nil } return false, err } return true, nil }