backend/pkg/chat/usecase/room.go

791 lines
21 KiB
Go
Raw Normal View History

2026-01-07 02:47:37 +00:00
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)
}