backend/pkg/chat/usecase/room.go

791 lines
21 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.

package usecase
import (
"backend/pkg/chat/domain/chat"
"backend/pkg/chat/domain/entity"
"backend/pkg/chat/domain/repository"
"backend/pkg/chat/domain/usecase"
"backend/pkg/library/cassandra"
errs "backend/pkg/library/errors"
"context"
"fmt"
"time"
"github.com/gocql/gocql"
)
const (
defaultRoomPageSize = 20
maxRoomPageSize = 100
defaultRoomStatus = "active"
defaultMemberRole = "member"
)
type RoomUseCaseParam struct {
RoomRepo repository.RoomRepository
Logger errs.Logger
}
type RoomUseCase struct {
RoomUseCaseParam
}
// NewRoomUseCase 創建新的聊天室 UseCase
func NewRoomUseCase(param RoomUseCaseParam) usecase.RoomUseCase {
return &RoomUseCase{
param,
}
}
// ==================== 聊天室管理 ====================
func (uc *RoomUseCase) CreateRoom(ctx context.Context, req usecase.CreateRoomReq) (*usecase.Room, error) {
// 驗證輸入參數
if err := uc.validateCreateRoomReq(req); err != nil {
return nil, err
}
// 設置預設值
status := req.Status
if status == "" {
status = defaultRoomStatus
}
// 創建聊天室實體
now := time.Now().UTC().UnixNano()
room := &entity.Room{
Name: req.Name,
Status: status,
CreatedAt: now,
UpdatedAt: now,
}
// 保存聊天室
if err := uc.RoomRepo.Create(ctx, room); err != nil {
return nil, uc.logError("RoomRepo.Create", req, err, "failed to create room")
}
// 將創建者添加為管理員
member := &entity.RoomMember{
RoomID: room.RoomID,
UID: req.UID,
Role: chat.RoomRoleAdmin.String(),
JoinedAt: now,
}
if err := uc.RoomRepo.Insert(ctx, member); err != nil {
// 如果添加成員失敗,嘗試刪除已創建的聊天室(清理)
_ = uc.RoomRepo.RoomDelete(ctx, room.RoomID.String())
return nil, uc.logError("RoomRepo.Insert", req, err, "failed to add creator as admin")
}
return uc.entityRoomToUseCase(room), nil
}
func (uc *RoomUseCase) GetRoom(ctx context.Context, req usecase.GetRoomReq) (*usecase.Room, error) {
// 驗證輸入參數
if err := uc.validateGetRoomReq(req); err != nil {
return nil, err
}
// 檢查用戶是否在聊天室中
isInRoom, err := uc.RoomRepo.IsUserInRoom(ctx, req.UID, req.RoomID)
if err != nil {
return nil, uc.logError("RoomRepo.IsUserInRoom", req, err, "failed to check user in room")
}
if !isInRoom {
return nil, errs.AuthForbiddenErrorL(
uc.Logger,
[]errs.LogField{
{Key: "uid", Val: req.UID},
{Key: "room_id", Val: req.RoomID},
},
"user is not in the room")
}
// 取得聊天室資訊
room, err := uc.RoomRepo.RoomGet(ctx, req.RoomID)
if err != nil {
if cassandra.IsNotFound(err) {
return nil, errs.ResNotFoundErrorL(
uc.Logger,
[]errs.LogField{
{Key: "room_id", Val: req.RoomID},
},
"room not found")
}
return nil, uc.logError("RoomRepo.RoomGet", req, err, "failed to get room")
}
return uc.entityRoomToUseCase(room), nil
}
func (uc *RoomUseCase) UpdateRoom(ctx context.Context, req usecase.UpdateRoomReq) (*usecase.Room, error) {
// 驗證輸入參數
if err := uc.validateUpdateRoomReq(req); err != nil {
return nil, err
}
// 檢查用戶是否在聊天室中且為管理員
member, err := uc.RoomRepo.Get(ctx, req.RoomID, req.UID)
if err != nil {
if cassandra.IsNotFound(err) {
return nil, errs.AuthForbiddenErrorL(
uc.Logger,
[]errs.LogField{
{Key: "uid", Val: req.UID},
{Key: "room_id", Val: req.RoomID},
},
"user is not in the room")
}
return nil, uc.logError("RoomRepo.Get", req, err, "failed to get member")
}
// 檢查是否為管理員
role := chat.RoomRole(member.Role)
if !role.IsAdmin() {
return nil, errs.AuthForbiddenErrorL(
uc.Logger,
[]errs.LogField{
{Key: "uid", Val: req.UID},
{Key: "room_id", Val: req.RoomID},
{Key: "role", Val: member.Role},
},
"only admin can update room")
}
// 取得現有聊天室資訊
room, err := uc.RoomRepo.RoomGet(ctx, req.RoomID)
if err != nil {
if cassandra.IsNotFound(err) {
return nil, errs.ResNotFoundErrorL(
uc.Logger,
[]errs.LogField{
{Key: "room_id", Val: req.RoomID},
},
"room not found")
}
return nil, uc.logError("RoomRepo.RoomGet", req, err, "failed to get room")
}
// 更新欄位
if req.Name != nil {
room.Name = *req.Name
}
if req.Status != nil {
room.Status = *req.Status
}
room.UpdatedAt = time.Now().UTC().UnixNano()
// 保存更新
if err := uc.RoomRepo.RoomUpdate(ctx, room); err != nil {
return nil, uc.logError("RoomRepo.RoomUpdate", req, err, "failed to update room")
}
return uc.entityRoomToUseCase(room), nil
}
func (uc *RoomUseCase) DeleteRoom(ctx context.Context, req usecase.DeleteRoomReq) error {
// 驗證輸入參數
if err := uc.validateDeleteRoomReq(req); err != nil {
return err
}
// 檢查用戶是否在聊天室中且為管理員
member, err := uc.RoomRepo.Get(ctx, req.RoomID, req.UID)
if err != nil {
if cassandra.IsNotFound(err) {
return errs.AuthForbiddenErrorL(
uc.Logger,
[]errs.LogField{
{Key: "uid", Val: req.UID},
{Key: "room_id", Val: req.RoomID},
},
"user is not in the room")
}
return uc.logError("RoomRepo.Get", req, err, "failed to get member")
}
// 檢查是否為管理員
role := chat.RoomRole(member.Role)
if !role.IsAdmin() {
return errs.AuthForbiddenErrorL(
uc.Logger,
[]errs.LogField{
{Key: "uid", Val: req.UID},
{Key: "room_id", Val: req.RoomID},
{Key: "role", Val: member.Role},
},
"only admin can delete room")
}
// 刪除聊天室(會同時刪除相關的成員和訊息)
if err := uc.RoomRepo.RoomDelete(ctx, req.RoomID); err != nil {
if cassandra.IsNotFound(err) {
return errs.ResNotFoundErrorL(
uc.Logger,
[]errs.LogField{
{Key: "room_id", Val: req.RoomID},
},
"room not found")
}
return uc.logError("RoomRepo.RoomDelete", req, err, "failed to delete room")
}
return nil
}
func (uc *RoomUseCase) ListRooms(ctx context.Context, req usecase.ListRoomsReq) ([]usecase.Room, string, int64, error) {
// 驗證輸入參數
if err := uc.validateListRoomsReq(req); err != nil {
return nil, "", 0, err
}
// 設置預設值
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = defaultRoomPageSize
}
if pageSize > maxRoomPageSize {
pageSize = maxRoomPageSize
}
// 查詢用戶所在的聊天室
userRooms, err := uc.RoomRepo.GetUserRooms(ctx, req.UID)
if err != nil {
return nil, "", 0, uc.logError("RoomRepo.GetUserRooms", req, err, "failed to get user rooms")
}
// 如果沒有聊天室,直接返回
if len(userRooms) == 0 {
return []usecase.Room{}, "", 0, nil
}
// 取得聊天室 ID 列表
roomIDs := make([]string, 0, len(userRooms))
for _, ur := range userRooms {
roomIDs = append(roomIDs, ur.RoomID.String())
}
// 根據 ID 列表查詢聊天室詳細資訊
rooms, err := uc.RoomRepo.RoomGetByID(ctx, roomIDs)
if err != nil {
return nil, "", 0, uc.logError("RoomRepo.RoomGetByID", req, err, "failed to get rooms by id")
}
// 篩選狀態
if req.Status != "" {
filtered := make([]entity.Room, 0, len(rooms))
for _, room := range rooms {
if room.Status == req.Status {
filtered = append(filtered, room)
}
}
rooms = filtered
}
// 轉換為 usecase.Room
result := make([]usecase.Room, 0, len(rooms))
for _, room := range rooms {
result = append(result, *uc.entityRoomToUseCase(&room))
}
// 計算總數(僅第一頁)
var total int64
if req.LastID == "" {
total = int64(len(result))
}
// 返回最後一個 ID用於分頁
lastID := ""
if len(result) > 0 {
lastID = result[len(result)-1].RoomID
}
return result, lastID, total, nil
}
func (uc *RoomUseCase) IsUserInRoom(ctx context.Context, req usecase.IsUserInRoomReq) (bool, error) {
// 驗證輸入參數
if err := uc.validateIsUserInRoomReq(req); err != nil {
return false, err
}
// 檢查用戶是否在聊天室中
isInRoom, err := uc.RoomRepo.IsUserInRoom(ctx, req.UID, req.RoomID)
if err != nil {
return false, uc.logError("RoomRepo.IsUserInRoom", req, err, "failed to check user in room")
}
return isInRoom, nil
}
// ==================== 成員管理 ====================
func (uc *RoomUseCase) AddMember(ctx context.Context, req usecase.AddMemberReq) (*usecase.Member, error) {
// 驗證輸入參數
if err := uc.validateAddMemberReq(req); err != nil {
return nil, err
}
// 檢查操作者是否在聊天室中且為管理員
operator, err := uc.RoomRepo.Get(ctx, req.RoomID, req.UID)
if err != nil {
if cassandra.IsNotFound(err) {
return nil, errs.AuthForbiddenErrorL(
uc.Logger,
[]errs.LogField{
{Key: "uid", Val: req.UID},
{Key: "room_id", Val: req.RoomID},
},
"operator is not in the room")
}
return nil, uc.logError("RoomRepo.Get", req, err, "failed to get operator")
}
// 檢查是否為管理員
operatorRole := chat.RoomRole(operator.Role)
if !operatorRole.IsAdmin() {
return nil, errs.AuthForbiddenErrorL(
uc.Logger,
[]errs.LogField{
{Key: "uid", Val: req.UID},
{Key: "room_id", Val: req.RoomID},
{Key: "role", Val: operator.Role},
},
"only admin can add members")
}
// 檢查要添加的用戶是否已在聊天室中
existing, err := uc.RoomRepo.Get(ctx, req.RoomID, req.MemberUID)
if err == nil && existing != nil {
return nil, errs.InputInvalidFormatError("user is already in the room")
}
// 設置預設角色
role := req.Role
if role == "" {
role = defaultMemberRole
}
// 驗證角色
roleEnum := chat.RoomRole(role)
if !roleEnum.IsValid() || roleEnum.IsOwner() {
return nil, errs.InputInvalidFormatError("invalid role")
}
// 創建成員實體
now := time.Now().UTC().UnixNano()
roomUUID, err := gocql.ParseUUID(req.RoomID)
if err != nil {
return nil, errs.InputInvalidFormatError("invalid room_id format")
}
member := &entity.RoomMember{
RoomID: roomUUID,
UID: req.MemberUID,
Role: role,
JoinedAt: now,
}
// 添加到聊天室
if err := uc.RoomRepo.Insert(ctx, member); err != nil {
return nil, uc.logError("RoomRepo.Insert", req, err, "failed to add member")
}
return uc.entityMemberToUseCase(member), nil
}
func (uc *RoomUseCase) RemoveMember(ctx context.Context, req usecase.RemoveMemberReq) error {
// 驗證輸入參數
if err := uc.validateRemoveMemberReq(req); err != nil {
return err
}
// 檢查操作者是否在聊天室中
operator, err := uc.RoomRepo.Get(ctx, req.RoomID, req.UID)
if err != nil {
if cassandra.IsNotFound(err) {
return errs.AuthForbiddenErrorL(
uc.Logger,
[]errs.LogField{
{Key: "uid", Val: req.UID},
{Key: "room_id", Val: req.RoomID},
},
"operator is not in the room")
}
return uc.logError("RoomRepo.Get", req, err, "failed to get operator")
}
// 檢查要移除的成員
member, err := uc.RoomRepo.Get(ctx, req.RoomID, req.MemberUID)
if err != nil {
if cassandra.IsNotFound(err) {
return errs.ResNotFoundErrorL(
uc.Logger,
[]errs.LogField{
{Key: "member_uid", Val: req.MemberUID},
{Key: "room_id", Val: req.RoomID},
},
"member not found")
}
return uc.logError("RoomRepo.Get", req, err, "failed to get member")
}
// 檢查權限:管理員可以移除任何人,普通成員只能移除自己
operatorRole := chat.RoomRole(operator.Role)
if !operatorRole.IsAdmin() {
if req.UID != req.MemberUID {
return errs.AuthForbiddenErrorL(
uc.Logger,
[]errs.LogField{
{Key: "uid", Val: req.UID},
{Key: "member_uid", Val: req.MemberUID},
{Key: "room_id", Val: req.RoomID},
},
"only admin can remove other members")
}
}
// 不能移除擁有者
memberRole := chat.RoomRole(member.Role)
if memberRole.IsOwner() {
return errs.AuthForbiddenErrorL(
uc.Logger,
[]errs.LogField{
{Key: "member_uid", Val: req.MemberUID},
{Key: "room_id", Val: req.RoomID},
},
"cannot remove owner")
}
// 移除成員
if err := uc.RoomRepo.DeleteMember(ctx, req.RoomID, req.MemberUID); err != nil {
return uc.logError("RoomRepo.DeleteMember", req, err, "failed to remove member")
}
return nil
}
func (uc *RoomUseCase) UpdateMemberRole(ctx context.Context, req usecase.UpdateMemberRoleReq) (*usecase.Member, error) {
// 驗證輸入參數
if err := uc.validateUpdateMemberRoleReq(req); err != nil {
return nil, err
}
// 檢查操作者是否在聊天室中且為管理員
operator, err := uc.RoomRepo.Get(ctx, req.RoomID, req.UID)
if err != nil {
if cassandra.IsNotFound(err) {
return nil, errs.AuthForbiddenErrorL(
uc.Logger,
[]errs.LogField{
{Key: "uid", Val: req.UID},
{Key: "room_id", Val: req.RoomID},
},
"operator is not in the room")
}
return nil, uc.logError("RoomRepo.Get", req, err, "failed to get operator")
}
// 檢查是否為管理員
operatorRole := chat.RoomRole(operator.Role)
if !operatorRole.IsAdmin() {
return nil, errs.AuthForbiddenErrorL(
uc.Logger,
[]errs.LogField{
{Key: "uid", Val: req.UID},
{Key: "room_id", Val: req.RoomID},
{Key: "role", Val: operator.Role},
},
"only admin can update member role")
}
// 檢查要更新的成員
member, err := uc.RoomRepo.Get(ctx, req.RoomID, req.MemberUID)
if err != nil {
if cassandra.IsNotFound(err) {
return nil, errs.ResNotFoundErrorL(
uc.Logger,
[]errs.LogField{
{Key: "member_uid", Val: req.MemberUID},
{Key: "room_id", Val: req.RoomID},
},
"member not found")
}
return nil, uc.logError("RoomRepo.Get", req, err, "failed to get member")
}
// 驗證角色
roleEnum := chat.RoomRole(req.Role)
if !roleEnum.IsValid() || roleEnum.IsOwner() {
return nil, errs.InputInvalidFormatError("invalid role")
}
// 不能修改擁有者的角色
memberRole := chat.RoomRole(member.Role)
if memberRole.IsOwner() {
return nil, errs.AuthForbiddenErrorL(
uc.Logger,
[]errs.LogField{
{Key: "member_uid", Val: req.MemberUID},
{Key: "room_id", Val: req.RoomID},
},
"cannot update owner role")
}
// 更新角色
member.Role = req.Role
if err := uc.RoomRepo.UpdateRole(ctx, member); err != nil {
return nil, uc.logError("RoomRepo.UpdateRole", req, err, "failed to update member role")
}
return uc.entityMemberToUseCase(member), nil
}
func (uc *RoomUseCase) ListMembers(ctx context.Context, req usecase.ListMembersReq) ([]usecase.Member, int64, error) {
// 驗證輸入參數
if err := uc.validateListMembersReq(req); err != nil {
return nil, 0, err
}
// 檢查用戶是否在聊天室中
isInRoom, err := uc.RoomRepo.IsUserInRoom(ctx, req.UID, req.RoomID)
if err != nil {
return nil, 0, uc.logError("RoomRepo.IsUserInRoom", req, err, "failed to check user in room")
}
if !isInRoom {
return nil, 0, errs.AuthForbiddenErrorL(
uc.Logger,
[]errs.LogField{
{Key: "uid", Val: req.UID},
{Key: "room_id", Val: req.RoomID},
},
"user is not in the room")
}
// 查詢所有成員
members, err := uc.RoomRepo.AllMembers(ctx, req.RoomID)
if err != nil {
return nil, 0, uc.logError("RoomRepo.AllMembers", req, err, "failed to list members")
}
// 轉換為 usecase.Member
result := make([]usecase.Member, 0, len(members))
for _, member := range members {
result = append(result, *uc.entityMemberToUseCase(&member))
}
// 計算總數
total, err := uc.RoomRepo.Count(ctx, req.RoomID)
if err != nil {
return nil, 0, uc.logError("RoomRepo.Count", req, err, "failed to count members")
}
return result, total, nil
}
// ==================== 用戶相關 ====================
func (uc *RoomUseCase) GetUserRooms(ctx context.Context, req usecase.GetUserRoomsReq) ([]usecase.Room, int64, error) {
// 驗證輸入參數
if err := uc.validateGetUserRoomsReq(req); err != nil {
return nil, 0, err
}
// 查詢用戶所在的聊天室
userRooms, err := uc.RoomRepo.GetUserRooms(ctx, req.UID)
if err != nil {
return nil, 0, uc.logError("RoomRepo.GetUserRooms", req, err, "failed to get user rooms")
}
// 如果沒有聊天室,直接返回
if len(userRooms) == 0 {
return []usecase.Room{}, 0, nil
}
// 取得聊天室 ID 列表
roomIDs := make([]string, 0, len(userRooms))
for _, ur := range userRooms {
roomIDs = append(roomIDs, ur.RoomID.String())
}
// 根據 ID 列表查詢聊天室詳細資訊
rooms, err := uc.RoomRepo.RoomGetByID(ctx, roomIDs)
if err != nil {
return nil, 0, uc.logError("RoomRepo.RoomGetByID", req, err, "failed to get rooms by id")
}
// 轉換為 usecase.Room
result := make([]usecase.Room, 0, len(rooms))
for _, room := range rooms {
result = append(result, *uc.entityRoomToUseCase(&room))
}
// 計算總數
total := int64(len(result))
return result, total, nil
}
// ==================== 輔助方法 ====================
func (uc *RoomUseCase) entityRoomToUseCase(room *entity.Room) *usecase.Room {
return &usecase.Room{
RoomID: room.RoomID.String(),
Name: room.Name,
Status: room.Status,
CreatedAt: room.CreatedAt,
UpdatedAt: room.UpdatedAt,
}
}
func (uc *RoomUseCase) entityMemberToUseCase(member *entity.RoomMember) *usecase.Member {
return &usecase.Member{
RoomID: member.RoomID.String(),
UID: member.UID,
Role: member.Role,
JoinedAt: member.JoinedAt,
}
}
// ==================== 驗證方法 ====================
func (uc *RoomUseCase) validateCreateRoomReq(req usecase.CreateRoomReq) error {
if req.Name == "" {
return errs.InputInvalidFormatError("name cannot be empty")
}
if req.UID == "" {
return errs.InputInvalidFormatError("uid cannot be empty")
}
if req.Status != "" && req.Status != "active" && req.Status != "archived" {
return errs.InputInvalidFormatError("invalid status")
}
return nil
}
func (uc *RoomUseCase) validateGetRoomReq(req usecase.GetRoomReq) error {
if req.RoomID == "" {
return errs.InputInvalidFormatError("room_id cannot be empty")
}
if req.UID == "" {
return errs.InputInvalidFormatError("uid cannot be empty")
}
return nil
}
func (uc *RoomUseCase) validateUpdateRoomReq(req usecase.UpdateRoomReq) error {
if req.RoomID == "" {
return errs.InputInvalidFormatError("room_id cannot be empty")
}
if req.UID == "" {
return errs.InputInvalidFormatError("uid cannot be empty")
}
if req.Name != nil && *req.Name == "" {
return errs.InputInvalidFormatError("name cannot be empty")
}
if req.Status != nil && *req.Status != "active" && *req.Status != "archived" {
return errs.InputInvalidFormatError("invalid status")
}
return nil
}
func (uc *RoomUseCase) validateDeleteRoomReq(req usecase.DeleteRoomReq) error {
if req.RoomID == "" {
return errs.InputInvalidFormatError("room_id cannot be empty")
}
if req.UID == "" {
return errs.InputInvalidFormatError("uid cannot be empty")
}
return nil
}
func (uc *RoomUseCase) validateListRoomsReq(req usecase.ListRoomsReq) error {
if req.UID == "" {
return errs.InputInvalidFormatError("uid cannot be empty")
}
return nil
}
func (uc *RoomUseCase) validateIsUserInRoomReq(req usecase.IsUserInRoomReq) error {
if req.UID == "" {
return errs.InputInvalidFormatError("uid cannot be empty")
}
if req.RoomID == "" {
return errs.InputInvalidFormatError("room_id cannot be empty")
}
return nil
}
func (uc *RoomUseCase) validateAddMemberReq(req usecase.AddMemberReq) error {
if req.RoomID == "" {
return errs.InputInvalidFormatError("room_id cannot be empty")
}
if req.UID == "" {
return errs.InputInvalidFormatError("uid cannot be empty")
}
if req.MemberUID == "" {
return errs.InputInvalidFormatError("member_uid cannot be empty")
}
return nil
}
func (uc *RoomUseCase) validateRemoveMemberReq(req usecase.RemoveMemberReq) error {
if req.RoomID == "" {
return errs.InputInvalidFormatError("room_id cannot be empty")
}
if req.UID == "" {
return errs.InputInvalidFormatError("uid cannot be empty")
}
if req.MemberUID == "" {
return errs.InputInvalidFormatError("member_uid cannot be empty")
}
return nil
}
func (uc *RoomUseCase) validateUpdateMemberRoleReq(req usecase.UpdateMemberRoleReq) error {
if req.RoomID == "" {
return errs.InputInvalidFormatError("room_id cannot be empty")
}
if req.UID == "" {
return errs.InputInvalidFormatError("uid cannot be empty")
}
if req.MemberUID == "" {
return errs.InputInvalidFormatError("member_uid cannot be empty")
}
if req.Role == "" {
return errs.InputInvalidFormatError("role cannot be empty")
}
return nil
}
func (uc *RoomUseCase) validateListMembersReq(req usecase.ListMembersReq) error {
if req.RoomID == "" {
return errs.InputInvalidFormatError("room_id cannot be empty")
}
if req.UID == "" {
return errs.InputInvalidFormatError("uid cannot be empty")
}
return nil
}
func (uc *RoomUseCase) validateGetUserRoomsReq(req usecase.GetUserRoomsReq) error {
if req.UID == "" {
return errs.InputInvalidFormatError("uid cannot be empty")
}
return nil
}
// ==================== 錯誤日誌 ====================
func (uc *RoomUseCase) logError(funcName string, req interface{}, err error, message string) error {
return errs.DBErrorErrorL(
uc.Logger,
[]errs.LogField{
{Key: "func", Val: funcName},
{Key: "req", Val: fmt.Sprintf("%+v", req)},
{Key: "err", Val: err.Error()},
},
message,
).Wrap(err)
}