diff --git a/etc/gateway.yaml b/etc/gateway.yaml index 552284e..a2df0dc 100644 --- a/etc/gateway.yaml +++ b/etc/gateway.yaml @@ -75,4 +75,15 @@ AmazonS3Settings: BucketURI: https://gutenbergtw-prod.s3.ap-northeast-3.amazonaws.com AccessKey: AKIAVRUVVY4IJOBFOY42 SecretKey: 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 - CloudFrontID: E3UMOQ0CGBOBAE \ No newline at end of file + CloudFrontID: E3UMOQ0CGBOBAE +Centrifugo: + APIURL: http://localhost:8000/api + APIKey: "api-key" +Cassandra: + Hosts: + - localhost + Port: 9042 + Keyspace: chat + Username: "cassandra" + Password: "cassandra" + UseAuth: false \ No newline at end of file diff --git a/generate/api/chat.api b/generate/api/chat.api new file mode 100644 index 0000000..675ddb1 --- /dev/null +++ b/generate/api/chat.api @@ -0,0 +1,351 @@ +syntax = "v1" + +// ================================================================= +// Type: 聊天室 (Chat Room) +// ================================================================= +type ( + // CreateRoomReq 創建聊天室請求 + CreateRoomReq { + Authorization + Name string `json:"name" validate:"required,min=1,max=100"` // 聊天室名稱 + Status string `json:"status,optional" validate:"omitempty,oneof=active archived"` // 狀態,預設為 active + } + + // UpdateRoomReq 更新聊天室請求 + UpdateRoomReq { + Authorization + RoomID string `path:"room_id" validate:"required"` + Name *string `json:"name,optional" validate:"omitempty,min=1,max=100"` + Status *string `json:"status,optional" validate:"omitempty,oneof=active archived"` + } + + // RoomResp 聊天室回應 + RoomResp { + RoomID string `json:"room_id"` + Name string `json:"name"` + Status string `json:"status"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + } + + // ListRoomsReq 查詢聊天室列表請求 + ListRoomsReq { + Authorization + Status string `json:"status,optional" validate:"omitempty,oneof=active archived"` // 狀態篩選 + PageSize int `json:"page_size,optional" validate:"omitempty,min=1,max=100"` // 每頁大小,預設 20 + LastID string `json:"last_id,optional"` // 用於 cursor-based pagination + } + + // ListRoomsResp 聊天室列表回應 + ListRoomsResp { + Rooms []RoomResp `json:"rooms"` + LastID string `json:"last_id,optional"` // 用於下一頁查詢 + Total int64 `json:"total,optional"` // 總數(僅第一頁返回) + } + + // AddMemberReq 添加成員請求 + AddMemberReq { + Authorization + RoomID string `json:"room_id" validate:"required"` + UID string `json:"uid" validate:"required"` // 要添加的用戶 UID + } + + // RemoveMemberReq 移除成員請求 + RemoveMemberReq { + Authorization + RoomID string `path:"room_id" validate:"required"` + UID string `path:"uid" validate:"required"` // 要移除的用戶 UID + } + + // UpdateMemberRoleReq 更新成員角色請求 + UpdateMemberRoleReq { + Authorization + RoomID string `path:"room_id" validate:"required"` + UID string `path:"uid" validate:"required"` + Role string `json:"role" validate:"required,oneof=admin member"` + } + + // MemberResp 成員回應 + MemberResp { + RoomID string `json:"room_id"` + UID string `json:"uid"` + Role string `json:"role"` + JoinedAt string `json:"joined_at"` + UpdatedAt string `json:"updated_at"` + } + + // ListMembersResp 成員列表回應 + ListMembersResp { + Members []Member `json:"members"` + Total int64 `json:"total"` + } + + // MemberResp 成員回應 + Member { + UID string `json:"uid"` + Name string `json:"name"` + Avatar string `json:"avatar"` + } + + // GetUserRoomsResp 用戶聊天室列表回應 + GetUserRoomsResp { + Rooms []RoomResp `json:"rooms"` + Total int64 `json:"total"` + } + + RoomReq { + Authorization + RoomID string `path:"room_id"` + } +) + +// ================================================================= +// Type: 訊息 (Message) +// ================================================================= +type ( + // SendMessageReq 發送訊息請求 + SendMessageReq { + Authorization + RoomID string `path:"room_id" validate:"required"` + Content string `json:"content" validate:"required,min=1,max=5000"` // 訊息內容 + } + + // SendMessageResp 發送訊息回應 + SendMessageResp { + RoomID string `json:"room_id"` + BucketDay string `json:"bucket_day"` // yyyyMMdd + TS int64 `json:"ts"` // timestamp + UID string `json:"uid"` + Content string `json:"content"` + } + + // ListMessagesReq 查詢訊息列表請求 + ListMessagesReq { + Authorization + RoomID string `path:"room_id" validate:"required"` + BucketDay string `json:"bucket_day,optional"` // yyyyMMdd,不提供則使用今天 + PageSize int64 `json:"page_size,optional" validate:"omitempty,min=1,max=100"` // 每頁大小,預設 20 + LastTS int64 `json:"last_ts,optional"` // 用於 cursor-based pagination + } + + // ListMessagesResp 訊息列表回應 + ListMessagesResp { + Messages []MessageResp `json:"messages"` + Total int64 `json:"total,optional"` // 總數(僅第一頁返回) + LastTS int64 `json:"last_ts,optional"` // 用於下一頁查詢 + } + + // MessageResp 訊息回應 + MessageResp { + RoomID string `json:"room_id"` + BucketDay string `json:"bucket_day"` // yyyyMMdd + TS int64 `json:"ts"` // timestamp + UID string `json:"uid"` + Content string `json:"content"` + } + + + IsMemberResp { + IsMember bool `json:"is_member"` + } +) + +// ================================================================= +// Service: 聊天 API - 需要登入 (Chat Service) +// ================================================================= +@server( + group: chat + prefix: /api/v1/chat + schemes: https + timeout: 30s + middleware: AuthMiddleware +) +service gateway { + // ==================== 聊天室管理 ==================== + @doc( + summary: "創建聊天室" + description: "創建一個新的聊天室,創建者自動成為管理員" + ) + /* + @respdoc-200 (RoomResp) // 建立成功 + @respdoc-400 (ErrorResp) "請求參數格式錯誤" + @respdoc-403 (ErrorResp) "沒有登入" + @respdoc-422 (ErrorResp) "創建太多聊天室了(目前仙梅設定上限)" + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler createRoom + post /rooms (CreateRoomReq) returns (RoomResp) + + @doc( + summary: "取得聊天室資訊" + description: "根據 room_id 取得聊天室的詳細資訊" + ) + /* + @respdoc-200 (RoomResp) // 取得聊天室 + @respdoc-400 (ErrorResp) "請求參數格式錯誤" + @respdoc-401 (ErrorResp) "不再房間內的人" + @respdoc-403 (ErrorResp) "沒有登入" + @respdoc-404 (ErrorResp) "找不到聊天室" + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler getRoom + get /rooms/:room_id (RoomReq) returns (RoomResp) + + @doc( + summary: "更新聊天室資訊" + description: "更新聊天室的名稱或狀態,需要管理員權限" + ) + /* + @respdoc-200 (RoomResp) // 取得聊天室 + @respdoc-400 (ErrorResp) "請求參數格式錯誤" + @respdoc-401 (ErrorResp) "不再房間內的人" + @respdoc-403 (ErrorResp) "沒有登入" + @respdoc-404 (ErrorResp) "找不到聊天室" + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler updateRoom + put /rooms/:room_id (UpdateRoomReq) returns (RoomResp) + + @doc( + summary: "刪除聊天室" + description: "刪除聊天室及其所有成員和訊息,需要管理員權限" + ) + /* + @respdoc-200 (RespOK) + @respdoc-400 (ErrorResp) "請求參數格式錯誤" + @respdoc-401 (ErrorResp) "權限不夠" + @respdoc-403 (ErrorResp) "沒有登入" + @respdoc-404 (ErrorResp) "找不到聊天室" + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler deleteRoom + delete /rooms/:room_id (RoomReq) returns (RespOK) + + @doc( + summary: "查詢聊天室列表" + description: "查詢聊天室列表,支援狀態篩選和分頁(自己的,之後再支援管理員吃全部)" + ) + /* + @respdoc-200 (ListRoomsResp) // 取得聊天室列表 + @respdoc-400 (ErrorResp) "請求參數格式錯誤" + @respdoc-401 (ErrorResp) "權限不夠" + @respdoc-403 (ErrorResp) "沒有登入" + @respdoc-404 (ErrorResp) "找不到聊天室" + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler listRooms + get /rooms (ListRoomsReq) returns (ListRoomsResp) + + // ==================== 成員管理 ==================== + @doc( + summary: "添加成員到聊天室" + description: "將用戶添加到聊天室,需要管理員權限" + ) + /* + @respdoc-200 (MemberResp) // 取得聊天室列表 + @respdoc-400 (ErrorResp) "請求參數格式錯誤" + @respdoc-401 (ErrorResp) "權限不夠" + @respdoc-403 (ErrorResp) "沒有登入" + @respdoc-404 (ErrorResp) "找不到聊天室" + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler addMember + post /rooms/:room_id/members (AddMemberReq) returns (MemberResp) + + @doc( + summary: "移除聊天室成員" + description: "將成員從聊天室中移除,需要管理員權限或本人操作" + ) + /* + @respdoc-200 (RespOK) + @respdoc-401 (ErrorResp) "權限不夠" + @respdoc-403 (ErrorResp) "沒有登入" + @respdoc-404 (ErrorResp) "找不到聊天室" + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler removeMember + delete /rooms/:room_id/members/:uid (RemoveMemberReq) returns (RespOK) + + @doc( + summary: "更新成員角色" + description: "更新成員在聊天室中的角色,需要管理員權限" + ) + /* + @respdoc-200 (MemberResp) + @respdoc-401 (ErrorResp) "權限不夠" + @respdoc-403 (ErrorResp) "沒有登入" + @respdoc-404 (ErrorResp) "找不到聊天室" + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler updateMemberRole + put /rooms/:room_id/members/:uid/role (UpdateMemberRoleReq) returns (MemberResp) + + @doc( + summary: "查詢聊天室成員列表" + description: "查詢指定聊天室的所有成員" + ) + /* + @respdoc-200 (ListMembersResp) + @respdoc-403 (ErrorResp) "沒有登入" + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler listMembers + get /rooms/:room_id/members (RoomReq) returns (ListMembersResp) + + // ==================== 用戶相關 ==================== + @doc( + summary: "查詢用戶所在的聊天室" + description: "查詢當前用戶或指定用戶所在的所有聊天室" + ) + /* + @respdoc-200 (GetUserRoomsResp) + @respdoc-401 (ErrorResp) "權限不夠" + @respdoc-403 (ErrorResp) "沒有登入" + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler getUserRooms + get /users/rooms (Authorization) returns (GetUserRoomsResp) + + @doc( + summary: "檢查用戶是否在聊天室中" + description: "檢查指定用戶是否在某個聊天室中" + ) + /* + @respdoc-200 (IsMemberResp) + @respdoc-401 (ErrorResp) "權限不夠" + @respdoc-403 (ErrorResp) "沒有登入" + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler checkUserInRoom + get /rooms/:room_id/members/:uid (RemoveMemberReq) returns (IsMemberResp) + + // ==================== 訊息相關 ==================== + @doc( + summary: "發送訊息" + description: "在聊天室中發送訊息,需要是聊天室成員" + ) + /* + @respdoc-200 (RespOK) + @respdoc-400 (ErrorResp) "請求參數格式錯誤" + @respdoc-401 (ErrorResp) "權限不夠" + @respdoc-403 (ErrorResp) "沒有登入" + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler sendMessage + post /rooms/:room_id/messages (SendMessageReq) returns (RespOK) + + @doc( + summary: "查詢訊息列表" + description: "查詢聊天室中的訊息列表,支援分頁和按日期篩選" + ) + /* + @respdoc-200 (ListMessagesResp) + @respdoc-400 (ErrorResp) "請求參數格式錯誤" + @respdoc-401 (ErrorResp) "權限不夠" + @respdoc-403 (ErrorResp) "沒有登入" + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler listMessages + get /rooms/:room_id/messages (ListMessagesReq) returns (ListMessagesResp) +} + diff --git a/generate/api/gateway.api b/generate/api/gateway.api index 12663fc..f31a390 100755 --- a/generate/api/gateway.api +++ b/generate/api/gateway.api @@ -17,5 +17,6 @@ import ( "ping.api" "member.api" "file_storage.api" + "chat.api" ) diff --git a/generate/api/notification.api b/generate/api/notification.api new file mode 100644 index 0000000..7e8cf88 --- /dev/null +++ b/generate/api/notification.api @@ -0,0 +1,222 @@ +syntax = "v1" + +// ================================================================= +// Type: 通知事件 (Notification Event) +// ================================================================= +type ( + // CreateEventReq 創建通知事件請求 + CreateEventReq { + Authorization + EventType string `json:"event_type" validate:"required"` // POST_PUBLISHED, COMMENT_ADDED, MENTIONED 等 + ActorUID string `json:"actor_uid" validate:"required"` // 觸發者 UID + ObjectType string `json:"object_type" validate:"required"` // POST, COMMENT, USER 等 + ObjectID string `json:"object_id" validate:"required"` // 對應物件 ID + Title string `json:"title" validate:"required"` // 顯示用標題 + Body string `json:"body" validate:"required"` // 顯示用內容/摘要 + Payload string `json:"payload,optional"` // JSON string(額外欄位) + Priority string `json:"priority,optional" validate:"omitempty,oneof=critical high normal low"` // 優先級,預設 normal + } + + // EventResp 通知事件回應 + EventResp { + EventID string `json:"event_id"` + EventType string `json:"event_type"` + ActorUID string `json:"actor_uid"` + ObjectType string `json:"object_type"` + ObjectID string `json:"object_id"` + Title string `json:"title"` + Body string `json:"body"` + Payload string `json:"payload"` + Priority string `json:"priority"` + CreatedAt string `json:"created_at"` + } + + // ListEventsReq 查詢事件列表請求 + ListEventsReq { + Authorization + ObjectID string `json:"object_id,optional"` // 物件 ID 篩選 + ObjectType string `json:"object_type,optional"` // 物件類型篩選 + Limit int `json:"limit,optional" validate:"omitempty,min=1,max=100"` // 限制數量,預設 20 + } + + // ListEventsResp 事件列表回應 + ListEventsResp { + Events []EventResp `json:"events"` + Total int64 `json:"total,optional"` + } +) + +// ================================================================= +// Type: 用戶通知 (User Notification) +// ================================================================= +type ( + // CreateUserNotificationReq 創建用戶通知請求 + CreateUserNotificationReq { + Authorization + UserID string `json:"user_id" validate:"required"` // 收通知的人 + EventID string `json:"event_id" validate:"required"` // 對應 notification_event.event_id + TTL int `json:"ttl,optional"` // 過期時間(秒),預設 30 天 + } + + // BulkCreateNotificationsReq 批量創建通知請求 + BulkCreateNotificationsReq { + Authorization + UserIDs []string `json:"user_ids" validate:"required,min=1,max=100"` // 用戶 UID 列表 + EventID string `json:"event_id" validate:"required"` // 對應 notification_event.event_id + TTL int `json:"ttl,optional"` // 過期時間(秒),預設 30 天 + } + + // ListNotificationsReq 查詢通知列表請求 + ListNotificationsReq { + Authorization + Buckets []string `json:"buckets,optional"` // 分桶列表,例如 ["2025-11", "2025-10"],不提供則使用最近 3 個月 + Limit int `json:"limit,optional" validate:"omitempty,min=1,max=100"` // 限制數量,預設 20 + } + + // NotificationResp 用戶通知回應 + NotificationResp { + UserID string `json:"user_id"` + Bucket string `json:"bucket"` // 分桶,例如 '2025-11' 或 '2025-11-17' + TS string `json:"ts"` // 通知時間,排序用(UTC0) + EventID string `json:"event_id"` // 對應 notification_event.event_id + Status string `json:"status"` // UNREAD / READ / ARCHIVED + ReadAt *string `json:"read_at,omitempty"` // 已讀時間(非必填) + Event EventResp `json:"event,omitempty"` // 關聯的事件資訊(可選) + } + + // ListNotificationsResp 通知列表回應 + ListNotificationsResp { + Notifications []NotificationResp `json:"notifications"` + Total int64 `json:"total,optional"` + UnreadCount int64 `json:"unread_count"` // 未讀數量 + } + + // MarkAsReadReq 標記已讀請求 + MarkAsReadReq { + Authorization + Bucket string `json:"bucket" validate:"required"` // 分桶,例如 '2025-11-17' + TS string `json:"ts" validate:"required"` // 通知時間戳 + } + + // MarkAllAsReadReq 標記全部已讀請求 + MarkAllAsReadReq { + Authorization + Buckets []string `json:"buckets,optional"` // 分桶列表,不提供則標記所有 + } + + // CountUnreadResp 未讀數量回應 + CountUnreadResp { + Count int64 `json:"count"` + } +) + +// ================================================================= +// Type: 通知游標 (Notification Cursor) +// ================================================================= +type ( + // UpdateCursorReq 更新游標請求 + UpdateCursorReq { + Authorization + LastSeenTS string `json:"last_seen_ts" validate:"required"` // 最後看到的時間戳 + } + + // CursorResp 游標回應 + CursorResp { + UID string `json:"uid"` + LastSeenTS string `json:"last_seen_ts"` + UpdatedAt string `json:"updated_at"` + } +) + +// ================================================================= +// Service: 通知 API - 需要登入 (Notification Service) +// ================================================================= +@server( + group: notification + prefix: /api/v1/notifications + schemes: https + timeout: 30s + middleware: AuthMiddleware +) +service gateway { + // ==================== 通知事件 ==================== + @doc( + summary: "創建通知事件" + description: "創建一個新的通知事件,通常由系統內部調用" + ) + @handler createEvent + post /events (CreateEventReq) returns (EventResp) + + @doc( + summary: "取得通知事件" + description: "根據 event_id 取得通知事件的詳細資訊" + ) + @handler getEvent + get /events/:event_id (Authorization) returns (EventResp) + + @doc( + summary: "查詢通知事件列表" + description: "查詢通知事件列表,支援按物件篩選" + ) + @handler listEvents + get /events (ListEventsReq) returns (ListEventsResp) + + // ==================== 用戶通知 ==================== + @doc( + summary: "創建用戶通知" + description: "為單個用戶創建通知" + ) + @handler createUserNotification + post /users/:user_id/notifications (CreateUserNotificationReq) returns (NotificationResp) + + @doc( + summary: "批量創建用戶通知" + description: "為多個用戶批量創建通知" + ) + @handler bulkCreateNotifications + post /notifications/bulk (BulkCreateNotificationsReq) returns (RespOK) + + @doc( + summary: "查詢用戶通知列表" + description: "查詢當前用戶的通知列表,支援按分桶篩選和分頁" + ) + @handler listNotifications + get /me/notifications (ListNotificationsReq) returns (ListNotificationsResp) + + @doc( + summary: "標記通知為已讀" + description: "標記單個通知為已讀狀態" + ) + @handler markAsRead + put /me/notifications/:bucket/:ts/read (MarkAsReadReq) returns (RespOK) + + @doc( + summary: "標記所有通知為已讀" + description: "標記指定分桶或所有通知為已讀狀態" + ) + @handler markAllAsRead + put /me/notifications/read (MarkAllAsReadReq) returns (RespOK) + + @doc( + summary: "查詢未讀通知數量" + description: "查詢當前用戶的未讀通知數量" + ) + @handler countUnread + get /me/notifications/unread/count (Authorization) returns (CountUnreadResp) + + // ==================== 通知游標 ==================== + @doc( + summary: "取得通知游標" + description: "取得當前用戶的通知游標資訊" + ) + @handler getCursor + get /me/notifications/cursor (Authorization) returns (CursorResp) + + @doc( + summary: "更新通知游標" + description: "更新當前用戶的通知游標,用於追蹤最後查看的通知位置" + ) + @handler updateCursor + put /me/notifications/cursor (UpdateCursorReq) returns (CursorResp) +} + diff --git a/generate/api/permission.api b/generate/api/permission.api new file mode 100644 index 0000000..cd986d3 --- /dev/null +++ b/generate/api/permission.api @@ -0,0 +1,376 @@ +syntax = "v1" + +// ================================================================= +// Type: 權限 (Permission) +// ================================================================= +type ( + // PermissionResp 權限回應 + PermissionResp { + ID string `json:"id"` + ParentID string `json:"parent_id"` + Name string `json:"name"` + HTTPPath string `json:"http_path,omitempty"` + HTTPMethod string `json:"http_method,omitempty"` + Status string `json:"status"` // active, inactive + Type string `json:"type"` // menu, button, api + } + + // PermissionTreeNode 權限樹節點 + PermissionTreeNode { + PermissionResp + Children []PermissionTreeNode `json:"children,omitempty"` + } + + // GetPermissionByHTTPReq 根據 HTTP 資訊查詢權限請求 + GetPermissionByHTTPReq { + Authorization + Path string `json:"path" validate:"required"` // HTTP 路徑 + Method string `json:"method" validate:"required"` // HTTP 方法 + } + + // ExpandPermissionsReq 展開權限請求 + ExpandPermissionsReq { + Authorization + Permissions []string `json:"permissions" validate:"required,min=1"` // 權限名稱列表 + } + + // ExpandPermissionsResp 展開權限回應 + ExpandPermissionsResp { + Permissions []string `json:"permissions"` // 展開後的權限列表(包含父權限) + } + + // GetUsersByPermissionReq 根據權限查詢用戶請求 + GetUsersByPermissionReq { + Authorization + Permissions []string `json:"permissions" validate:"required,min=1"` // 權限名稱列表 + } + + // GetUsersByPermissionResp 根據權限查詢用戶回應 + GetUsersByPermissionResp { + UserUIDs []string `json:"user_uids"` // 擁有指定權限的用戶 UID 列表 + } + + // ListPermissionsResp 權限列表回應 + ListPermissionsResp { + Permissions []PermissionResp `json:"permissions"` + Total int64 `json:"total"` + } +) + +// ================================================================= +// Type: 角色 (Role) +// ================================================================= +type ( + // CreateRoleReq 創建角色請求 + CreateRoleReq { + Authorization + ClientID int `json:"client_id" validate:"required"` + Name string `json:"name" validate:"required,min=1,max=100"` + Permissions map[string]string `json:"permissions,optional"` // 權限映射,key 為權限名稱,value 為狀態 (open, close) + } + + // UpdateRoleReq 更新角色請求 + UpdateRoleReq { + Authorization + Name *string `json:"name,optional" validate:"omitempty,min=1,max=100"` + Status *string `json:"status,optional" validate:"omitempty,oneof=active inactive"` + Permissions map[string]string `json:"permissions,optional"` // 權限映射 + } + + // RoleResp 角色回應 + RoleResp { + ID string `json:"id"` + UID string `json:"uid"` + ClientID int `json:"client_id"` + Name string `json:"name"` + Status string `json:"status"` // active, inactive + Permissions map[string]string `json:"permissions"` // 權限映射 + CreateTime string `json:"create_time"` + UpdateTime string `json:"update_time"` + } + + // RoleWithUserCountResp 角色回應(含用戶數量) + RoleWithUserCountResp { + RoleResp + UserCount int `json:"user_count"` + } + + // ListRolesReq 查詢角色列表請求 + ListRolesReq { + Authorization + ClientID int `json:"client_id,optional"` + Name string `json:"name,optional"` + Status string `json:"status,optional" validate:"omitempty,oneof=active inactive"` + Permissions []string `json:"permissions,optional"` // 權限名稱列表(篩選擁有這些權限的角色) + } + + // ListRolesResp 角色列表回應 + ListRolesResp { + Roles []RoleWithUserCountResp `json:"roles"` + Total int64 `json:"total"` + } + + // PageRolesReq 分頁查詢角色請求 + PageRolesReq { + Authorization + ClientID int `json:"client_id,optional"` + Name string `json:"name,optional"` + Status string `json:"status,optional" validate:"omitempty,oneof=active inactive"` + Permissions []string `json:"permissions,optional"` + Page int `json:"page,optional" validate:"omitempty,min=1"` // 頁碼,從 1 開始 + Size int `json:"size,optional" validate:"omitempty,min=1,max=100"` // 每頁大小 + } + + // PageRolesResp 角色分頁回應 + PageRolesResp { + List []RoleWithUserCountResp `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + Size int `json:"size"` + } +) + +// ================================================================= +// Type: 角色權限 (Role Permission) +// ================================================================= +type ( + // GetRolePermissionsResp 角色權限回應 + GetRolePermissionsResp { + RoleUID string `json:"role_uid"` + Permissions map[string]string `json:"permissions"` // 權限映射 + } + + // GetUserPermissionsResp 用戶權限回應 + GetUserPermissionsResp { + UserUID string `json:"user_uid"` + RoleUID string `json:"role_uid"` + RoleName string `json:"role_name"` + Permissions map[string]string `json:"permissions"` // 權限映射 + } + + // UpdateRolePermissionsReq 更新角色權限請求 + UpdateRolePermissionsReq { + Authorization + Permissions map[string]string `json:"permissions" validate:"required"` // 權限映射 + } + + // CheckPermissionReq 檢查權限請求 + CheckPermissionReq { + Authorization + Path string `json:"path" validate:"required"` // HTTP 路徑 + Method string `json:"method" validate:"required"` // HTTP 方法 + } + + // CheckPermissionResp 檢查權限回應 + CheckPermissionResp { + Allowed bool `json:"allowed"` // 是否有權限 + PermissionName string `json:"permission_name,omitempty"` // 權限名稱 + PlainCode bool `json:"plain_code"` // 是否有 plain_code 權限(特殊邏輯) + } +) + +// ================================================================= +// Type: 用戶角色 (User Role) +// ================================================================= +type ( + // AssignRoleReq 指派角色請求 + AssignRoleReq { + Authorization + UserUID string `json:"user_uid" validate:"required"` + RoleUID string `json:"role_uid" validate:"required"` + Brand string `json:"brand,optional"` // 品牌標識 + } + + // UserRoleResp 用戶角色回應 + UserRoleResp { + UserUID string `json:"user_uid"` + RoleUID string `json:"role_uid"` + Brand string `json:"brand"` + CreateTime string `json:"create_time"` + UpdateTime string `json:"update_time"` + } + + // ListUserRolesReq 查詢用戶角色列表請求 + ListUserRolesReq { + Authorization + Brand string `json:"brand,optional"` + RoleID string `json:"role_id,optional"` + Status string `json:"status,optional" validate:"omitempty,oneof=active inactive"` + } + + // ListUserRolesResp 用戶角色列表回應 + ListUserRolesResp { + UserRoles []UserRoleResp `json:"user_roles"` + Total int64 `json:"total"` + } + + // GetUsersByRoleResp 角色用戶列表回應 + GetUsersByRoleResp { + UserRoles []UserRoleResp `json:"user_roles"` + Total int64 `json:"total"` + } +) + +// ================================================================= +// Service: 權限管理 API - 需要登入 (Permission Service) +// ================================================================= +@server( + group: permission + prefix: /api/v1/permissions + schemes: https + timeout: 30s + middleware: AuthMiddleware +) +service gateway { + // ==================== 權限管理 ==================== + @doc( + summary: "取得所有權限" + description: "取得系統中所有啟用的權限列表" + ) + @handler getAllPermissions + get / (Authorization) returns (ListPermissionsResp) + + @doc( + summary: "取得權限樹" + description: "取得以樹狀結構組織的權限列表" + ) + @handler getPermissionTree + get /tree (Authorization) returns (PermissionTreeNode) + + @doc( + summary: "根據 HTTP 資訊取得權限" + description: "根據 HTTP 路徑和方法取得對應的權限資訊" + ) + @handler getPermissionByHTTP + post /by-http (GetPermissionByHTTPReq) returns (PermissionResp) + + @doc( + summary: "展開權限" + description: "展開權限列表,包含所有父權限" + ) + @handler expandPermissions + post /expand (ExpandPermissionsReq) returns (ExpandPermissionsResp) + + @doc( + summary: "根據權限取得用戶" + description: "取得擁有指定權限的所有用戶 UID" + ) + @handler getUsersByPermission + post /users (GetUsersByPermissionReq) returns (GetUsersByPermissionResp) + + // ==================== 角色管理 ==================== + @doc( + summary: "創建角色" + description: "創建一個新角色並設定權限" + ) + @handler createRole + post /roles (CreateRoleReq) returns (RoleResp) + + @doc( + summary: "更新角色" + description: "更新角色的名稱、狀態或權限" + ) + @handler updateRole + put /roles/:uid (UpdateRoleReq) returns (RoleResp) + + @doc( + summary: "刪除角色" + description: "刪除指定角色(軟刪除,設為 inactive)" + ) + @handler deleteRole + delete /roles/:uid (Authorization) returns (RespOK) + + @doc( + summary: "取得角色" + description: "根據 UID 取得角色的詳細資訊" + ) + @handler getRole + get /roles/:uid (Authorization) returns (RoleResp) + + @doc( + summary: "查詢角色列表" + description: "查詢角色列表,支援多種篩選條件" + ) + @handler listRoles + get /roles (ListRolesReq) returns (ListRolesResp) + + @doc( + summary: "分頁查詢角色" + description: "分頁查詢角色列表,支援多種篩選條件" + ) + @handler pageRoles + get /roles/page (PageRolesReq) returns (PageRolesResp) + + // ==================== 角色權限管理 ==================== + @doc( + summary: "取得角色權限" + description: "取得指定角色的所有權限" + ) + @handler getRolePermissions + get /roles/:role_uid (Authorization) returns (GetRolePermissionsResp) + + @doc( + summary: "取得用戶權限" + description: "取得指定用戶的所有權限(透過角色)" + ) + @handler getUserPermissions + get /users/:user_uid (Authorization) returns (GetUserPermissionsResp) + + @doc( + summary: "更新角色權限" + description: "更新指定角色的權限列表" + ) + @handler updateRolePermissions + put /roles/:role_uid (UpdateRolePermissionsReq) returns (GetRolePermissionsResp) + + @doc( + summary: "檢查權限" + description: "檢查當前用戶是否有執行指定 HTTP 操作的權限" + ) + @handler checkPermission + post /check (CheckPermissionReq) returns (CheckPermissionResp) + + // ==================== 用戶角色管理 ==================== + @doc( + summary: "指派角色給用戶" + description: "為用戶指派一個角色" + ) + @handler assignRole + post /users/:user_uid/roles (AssignRoleReq) returns (UserRoleResp) + + @doc( + summary: "更新用戶角色" + description: "更新用戶的角色(替換現有角色)" + ) + @handler updateUserRole + put /users/:user_uid/roles/:role_uid (Authorization) returns (UserRoleResp) + + @doc( + summary: "移除用戶角色" + description: "移除用戶的角色" + ) + @handler removeUserRole + delete /users/:user_uid/roles (Authorization) returns (RespOK) + + @doc( + summary: "取得用戶角色" + description: "取得指定用戶的角色資訊" + ) + @handler getUserRole + get /users/:user_uid/roles (Authorization) returns (UserRoleResp) + + @doc( + summary: "取得角色的所有用戶" + description: "取得擁有指定角色的所有用戶" + ) + @handler getUsersByRole + get /roles/:role_uid/users (Authorization) returns (GetUsersByRoleResp) + + @doc( + summary: "查詢用戶角色列表" + description: "查詢用戶角色列表,支援多種篩選條件" + ) + @handler listUserRoles + get /user-roles (ListUserRolesReq) returns (ListUserRolesResp) +} + diff --git a/internal/config/config.go b/internal/config/config.go index 616d5a1..384dc04 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -111,12 +111,12 @@ type Config struct { // Cassandra 配置 Cassandra struct { - Hosts []string - Port int - Keyspace string - Username string - Password string - UseAuth bool + Hosts []string + Port int + Keyspace string + Username string + Password string + UseAuth bool } // Centrifugo 配置 diff --git a/internal/handler/chat/add_member_handler.go b/internal/handler/chat/add_member_handler.go new file mode 100644 index 0000000..6169c1c --- /dev/null +++ b/internal/handler/chat/add_member_handler.go @@ -0,0 +1,51 @@ +package chat + +import ( + errs "backend/pkg/library/errors" + "net/http" + + "backend/internal/logic/chat" + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 添加成員到聊天室 +func AddMemberHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AddMemberReq + if err := httpx.Parse(r, &req); err != nil { + e := errs.InputInvalidFormatError(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: err.Error(), + }) + + return + } + + //if err := svcCtx.Validate.ValidateAll(req); err != nil { + // e := errs.InvalidFormat(err.Error()) + // httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Status{ + // Code: int64(e.FullCode()), + // Message: err.Error(), + // }) + // + // return + //} + + l := chat.NewAddMemberLogic(r.Context(), svcCtx) + resp, err := l.AddMember(&req) + if err != nil { + e := errs.FromError(err) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp) + } + } +} diff --git a/internal/handler/chat/check_user_in_room_handler.go b/internal/handler/chat/check_user_in_room_handler.go new file mode 100644 index 0000000..bfc3cbb --- /dev/null +++ b/internal/handler/chat/check_user_in_room_handler.go @@ -0,0 +1,41 @@ +package chat + +import ( + errs "backend/pkg/library/errors" + "net/http" + + "backend/internal/logic/chat" + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 檢查用戶是否在聊天室中 +func CheckUserInRoomHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.RemoveMemberReq + if err := httpx.Parse(r, &req); err != nil { + e := errs.InputInvalidFormatError(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: err.Error(), + }) + + return + } + + l := chat.NewCheckUserInRoomLogic(r.Context(), svcCtx) + resp, err := l.CheckUserInRoom(&req) + if err != nil { + e := errs.FromError(err) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp) + } + } +} diff --git a/internal/handler/chat/create_room_handler.go b/internal/handler/chat/create_room_handler.go new file mode 100644 index 0000000..0842e50 --- /dev/null +++ b/internal/handler/chat/create_room_handler.go @@ -0,0 +1,41 @@ +package chat + +import ( + errs "backend/pkg/library/errors" + "net/http" + + "backend/internal/logic/chat" + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 創建聊天室 +func CreateRoomHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CreateRoomReq + if err := httpx.Parse(r, &req); err != nil { + e := errs.InputInvalidFormatError(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: err.Error(), + }) + + return + } + + l := chat.NewCreateRoomLogic(r.Context(), svcCtx) + resp, err := l.CreateRoom(&req) + if err != nil { + e := errs.FromError(err) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp) + } + } +} diff --git a/internal/handler/chat/delete_room_handler.go b/internal/handler/chat/delete_room_handler.go new file mode 100644 index 0000000..36897df --- /dev/null +++ b/internal/handler/chat/delete_room_handler.go @@ -0,0 +1,41 @@ +package chat + +import ( + errs "backend/pkg/library/errors" + "net/http" + + "backend/internal/logic/chat" + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 刪除聊天室 +func DeleteRoomHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.RoomReq + if err := httpx.Parse(r, &req); err != nil { + e := errs.InputInvalidFormatError(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: err.Error(), + }) + + return + } + + l := chat.NewDeleteRoomLogic(r.Context(), svcCtx) + resp, err := l.DeleteRoom(&req) + if err != nil { + e := errs.FromError(err) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp) + } + } +} diff --git a/internal/handler/chat/get_room_handler.go b/internal/handler/chat/get_room_handler.go new file mode 100644 index 0000000..01439e0 --- /dev/null +++ b/internal/handler/chat/get_room_handler.go @@ -0,0 +1,41 @@ +package chat + +import ( + errs "backend/pkg/library/errors" + "net/http" + + "backend/internal/logic/chat" + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 取得聊天室資訊 +func GetRoomHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.RoomReq + if err := httpx.Parse(r, &req); err != nil { + e := errs.InputInvalidFormatError(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: err.Error(), + }) + + return + } + + l := chat.NewGetRoomLogic(r.Context(), svcCtx) + resp, err := l.GetRoom(&req) + if err != nil { + e := errs.FromError(err) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp) + } + } +} diff --git a/internal/handler/chat/get_user_rooms_handler.go b/internal/handler/chat/get_user_rooms_handler.go new file mode 100644 index 0000000..67daddf --- /dev/null +++ b/internal/handler/chat/get_user_rooms_handler.go @@ -0,0 +1,41 @@ +package chat + +import ( + errs "backend/pkg/library/errors" + "net/http" + + "backend/internal/logic/chat" + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 查詢用戶所在的聊天室 +func GetUserRoomsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.Authorization + if err := httpx.Parse(r, &req); err != nil { + e := errs.InputInvalidFormatError(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: err.Error(), + }) + + return + } + + l := chat.NewGetUserRoomsLogic(r.Context(), svcCtx) + resp, err := l.GetUserRooms(&req) + if err != nil { + e := errs.FromError(err) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp) + } + } +} diff --git a/internal/handler/chat/list_members_handler.go b/internal/handler/chat/list_members_handler.go new file mode 100644 index 0000000..5f19a38 --- /dev/null +++ b/internal/handler/chat/list_members_handler.go @@ -0,0 +1,41 @@ +package chat + +import ( + errs "backend/pkg/library/errors" + "net/http" + + "backend/internal/logic/chat" + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 查詢聊天室成員列表 +func ListMembersHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.RoomReq + if err := httpx.Parse(r, &req); err != nil { + e := errs.InputInvalidFormatError(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: err.Error(), + }) + + return + } + + l := chat.NewListMembersLogic(r.Context(), svcCtx) + resp, err := l.ListMembers(&req) + if err != nil { + e := errs.FromError(err) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp) + } + } +} diff --git a/internal/handler/chat/list_messages_handler.go b/internal/handler/chat/list_messages_handler.go new file mode 100644 index 0000000..39f3541 --- /dev/null +++ b/internal/handler/chat/list_messages_handler.go @@ -0,0 +1,41 @@ +package chat + +import ( + errs "backend/pkg/library/errors" + "net/http" + + "backend/internal/logic/chat" + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 查詢訊息列表 +func ListMessagesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.ListMessagesReq + if err := httpx.Parse(r, &req); err != nil { + e := errs.InputInvalidFormatError(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: err.Error(), + }) + + return + } + + l := chat.NewListMessagesLogic(r.Context(), svcCtx) + resp, err := l.ListMessages(&req) + if err != nil { + e := errs.FromError(err) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp) + } + } +} diff --git a/internal/handler/chat/list_rooms_handler.go b/internal/handler/chat/list_rooms_handler.go new file mode 100644 index 0000000..d5f1177 --- /dev/null +++ b/internal/handler/chat/list_rooms_handler.go @@ -0,0 +1,41 @@ +package chat + +import ( + errs "backend/pkg/library/errors" + "net/http" + + "backend/internal/logic/chat" + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 查詢聊天室列表 +func ListRoomsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.ListRoomsReq + if err := httpx.Parse(r, &req); err != nil { + e := errs.InputInvalidFormatError(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: err.Error(), + }) + + return + } + + l := chat.NewListRoomsLogic(r.Context(), svcCtx) + resp, err := l.ListRooms(&req) + if err != nil { + e := errs.FromError(err) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp) + } + } +} diff --git a/internal/handler/chat/remove_member_handler.go b/internal/handler/chat/remove_member_handler.go new file mode 100644 index 0000000..f054312 --- /dev/null +++ b/internal/handler/chat/remove_member_handler.go @@ -0,0 +1,41 @@ +package chat + +import ( + errs "backend/pkg/library/errors" + "net/http" + + "backend/internal/logic/chat" + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 移除聊天室成員 +func RemoveMemberHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.RemoveMemberReq + if err := httpx.Parse(r, &req); err != nil { + e := errs.InputInvalidFormatError(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: err.Error(), + }) + + return + } + + l := chat.NewRemoveMemberLogic(r.Context(), svcCtx) + resp, err := l.RemoveMember(&req) + if err != nil { + e := errs.FromError(err) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp) + } + } +} diff --git a/internal/handler/chat/send_message_handler.go b/internal/handler/chat/send_message_handler.go new file mode 100644 index 0000000..197d98c --- /dev/null +++ b/internal/handler/chat/send_message_handler.go @@ -0,0 +1,41 @@ +package chat + +import ( + errs "backend/pkg/library/errors" + "net/http" + + "backend/internal/logic/chat" + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 發送訊息 +func SendMessageHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.SendMessageReq + if err := httpx.Parse(r, &req); err != nil { + e := errs.InputInvalidFormatError(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: err.Error(), + }) + + return + } + + l := chat.NewSendMessageLogic(r.Context(), svcCtx) + resp, err := l.SendMessage(&req) + if err != nil { + e := errs.FromError(err) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp) + } + } +} diff --git a/internal/handler/chat/update_member_role_handler.go b/internal/handler/chat/update_member_role_handler.go new file mode 100644 index 0000000..7a21cdb --- /dev/null +++ b/internal/handler/chat/update_member_role_handler.go @@ -0,0 +1,41 @@ +package chat + +import ( + errs "backend/pkg/library/errors" + "net/http" + + "backend/internal/logic/chat" + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 更新成員角色 +func UpdateMemberRoleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.UpdateMemberRoleReq + if err := httpx.Parse(r, &req); err != nil { + e := errs.InputInvalidFormatError(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: err.Error(), + }) + + return + } + + l := chat.NewUpdateMemberRoleLogic(r.Context(), svcCtx) + resp, err := l.UpdateMemberRole(&req) + if err != nil { + e := errs.FromError(err) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp) + } + } +} diff --git a/internal/handler/chat/update_room_handler.go b/internal/handler/chat/update_room_handler.go new file mode 100644 index 0000000..42f7d01 --- /dev/null +++ b/internal/handler/chat/update_room_handler.go @@ -0,0 +1,41 @@ +package chat + +import ( + errs "backend/pkg/library/errors" + "net/http" + + "backend/internal/logic/chat" + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 更新聊天室資訊 +func UpdateRoomHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.UpdateRoomReq + if err := httpx.Parse(r, &req); err != nil { + e := errs.InputInvalidFormatError(err.Error()) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: err.Error(), + }) + + return + } + + l := chat.NewUpdateRoomLogic(r.Context(), svcCtx) + resp, err := l.UpdateRoom(&req) + if err != nil { + e := errs.FromError(err) + httpx.WriteJsonCtx(r.Context(), w, e.HTTPStatus(), types.Resp{ + Code: e.DisplayCode(), + Message: e.Error(), + Error: e.Unwrap(), + }) + } else { + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp) + } + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index f7d60b8..040080b 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -1,5 +1,5 @@ // Code generated by goctl. DO NOT EDIT. -// goctl 1.9.0 +// goctl 1.8.5 package handler @@ -8,6 +8,7 @@ import ( "time" auth "backend/internal/handler/auth" + chat "backend/internal/handler/chat" fileStorage "backend/internal/handler/fileStorage" ping "backend/internal/handler/ping" user "backend/internal/handler/user" @@ -60,6 +61,94 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { rest.WithTimeout(10000*time.Millisecond), ) + server.AddRoutes( + rest.WithMiddlewares( + []rest.Middleware{serverCtx.AuthMiddleware}, + []rest.Route{ + { + // 創建聊天室 + Method: http.MethodPost, + Path: "/rooms", + Handler: chat.CreateRoomHandler(serverCtx), + }, + { + // 查詢聊天室列表 + Method: http.MethodGet, + Path: "/rooms", + Handler: chat.ListRoomsHandler(serverCtx), + }, + { + // 取得聊天室資訊 + Method: http.MethodGet, + Path: "/rooms/:room_id", + Handler: chat.GetRoomHandler(serverCtx), + }, + { + // 更新聊天室資訊 + Method: http.MethodPut, + Path: "/rooms/:room_id", + Handler: chat.UpdateRoomHandler(serverCtx), + }, + { + // 刪除聊天室 + Method: http.MethodDelete, + Path: "/rooms/:room_id", + Handler: chat.DeleteRoomHandler(serverCtx), + }, + { + // 添加成員到聊天室 + Method: http.MethodPost, + Path: "/rooms/:room_id/members", + Handler: chat.AddMemberHandler(serverCtx), + }, + { + // 查詢聊天室成員列表 + Method: http.MethodGet, + Path: "/rooms/:room_id/members", + Handler: chat.ListMembersHandler(serverCtx), + }, + { + // 移除聊天室成員 + Method: http.MethodDelete, + Path: "/rooms/:room_id/members/:uid", + Handler: chat.RemoveMemberHandler(serverCtx), + }, + { + // 檢查用戶是否在聊天室中 + Method: http.MethodGet, + Path: "/rooms/:room_id/members/:uid", + Handler: chat.CheckUserInRoomHandler(serverCtx), + }, + { + // 更新成員角色 + Method: http.MethodPut, + Path: "/rooms/:room_id/members/:uid/role", + Handler: chat.UpdateMemberRoleHandler(serverCtx), + }, + { + // 發送訊息 + Method: http.MethodPost, + Path: "/rooms/:room_id/messages", + Handler: chat.SendMessageHandler(serverCtx), + }, + { + // 查詢訊息列表 + Method: http.MethodGet, + Path: "/rooms/:room_id/messages", + Handler: chat.ListMessagesHandler(serverCtx), + }, + { + // 查詢用戶所在的聊天室 + Method: http.MethodGet, + Path: "/users/rooms", + Handler: chat.GetUserRoomsHandler(serverCtx), + }, + }..., + ), + rest.WithPrefix("/api/v1/chat"), + rest.WithTimeout(30000*time.Millisecond), + ) + server.AddRoutes( rest.WithMiddlewares( []rest.Middleware{serverCtx.AuthMiddleware}, diff --git a/internal/logic/chat/add_member_logic.go b/internal/logic/chat/add_member_logic.go new file mode 100644 index 0000000..4c67ef4 --- /dev/null +++ b/internal/logic/chat/add_member_logic.go @@ -0,0 +1,31 @@ +package chat + +import ( + "context" + + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AddMemberLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewAddMemberLogic 添加成員到聊天室 +func NewAddMemberLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AddMemberLogic { + return &AddMemberLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AddMemberLogic) AddMember(req *types.AddMemberReq) (resp *types.MemberResp, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/chat/check_user_in_room_logic.go b/internal/logic/chat/check_user_in_room_logic.go new file mode 100644 index 0000000..8260d57 --- /dev/null +++ b/internal/logic/chat/check_user_in_room_logic.go @@ -0,0 +1,31 @@ +package chat + +import ( + "context" + + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CheckUserInRoomLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 檢查用戶是否在聊天室中 +func NewCheckUserInRoomLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CheckUserInRoomLogic { + return &CheckUserInRoomLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CheckUserInRoomLogic) CheckUserInRoom(req *types.RemoveMemberReq) (resp *types.IsMemberResp, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/chat/create_room_logic.go b/internal/logic/chat/create_room_logic.go new file mode 100644 index 0000000..5998656 --- /dev/null +++ b/internal/logic/chat/create_room_logic.go @@ -0,0 +1,30 @@ +package chat + +import ( + "context" + + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateRoomLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewCreateRoomLogic 創建聊天室 +func NewCreateRoomLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateRoomLogic { + return &CreateRoomLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateRoomLogic) CreateRoom(req *types.CreateRoomReq) (resp *types.RoomResp, err error) { + + return +} diff --git a/internal/logic/chat/delete_room_logic.go b/internal/logic/chat/delete_room_logic.go new file mode 100644 index 0000000..7f15864 --- /dev/null +++ b/internal/logic/chat/delete_room_logic.go @@ -0,0 +1,31 @@ +package chat + +import ( + "context" + + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DeleteRoomLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 刪除聊天室 +func NewDeleteRoomLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteRoomLogic { + return &DeleteRoomLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteRoomLogic) DeleteRoom(req *types.RoomReq) (resp *types.RespOK, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/chat/get_room_logic.go b/internal/logic/chat/get_room_logic.go new file mode 100644 index 0000000..64a92eb --- /dev/null +++ b/internal/logic/chat/get_room_logic.go @@ -0,0 +1,31 @@ +package chat + +import ( + "context" + + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetRoomLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 取得聊天室資訊 +func NewGetRoomLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetRoomLogic { + return &GetRoomLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetRoomLogic) GetRoom(req *types.RoomReq) (resp *types.RoomResp, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/chat/get_user_rooms_logic.go b/internal/logic/chat/get_user_rooms_logic.go new file mode 100644 index 0000000..56ffbf5 --- /dev/null +++ b/internal/logic/chat/get_user_rooms_logic.go @@ -0,0 +1,31 @@ +package chat + +import ( + "context" + + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetUserRoomsLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 查詢用戶所在的聊天室 +func NewGetUserRoomsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserRoomsLogic { + return &GetUserRoomsLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetUserRoomsLogic) GetUserRooms(req *types.Authorization) (resp *types.GetUserRoomsResp, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/chat/list_members_logic.go b/internal/logic/chat/list_members_logic.go new file mode 100644 index 0000000..9fbcc6c --- /dev/null +++ b/internal/logic/chat/list_members_logic.go @@ -0,0 +1,31 @@ +package chat + +import ( + "context" + + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ListMembersLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 查詢聊天室成員列表 +func NewListMembersLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListMembersLogic { + return &ListMembersLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ListMembersLogic) ListMembers(req *types.RoomReq) (resp *types.ListMembersResp, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/chat/list_messages_logic.go b/internal/logic/chat/list_messages_logic.go new file mode 100644 index 0000000..2831a01 --- /dev/null +++ b/internal/logic/chat/list_messages_logic.go @@ -0,0 +1,31 @@ +package chat + +import ( + "context" + + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ListMessagesLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 查詢訊息列表 +func NewListMessagesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListMessagesLogic { + return &ListMessagesLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ListMessagesLogic) ListMessages(req *types.ListMessagesReq) (resp *types.ListMessagesResp, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/chat/list_rooms_logic.go b/internal/logic/chat/list_rooms_logic.go new file mode 100644 index 0000000..ce54529 --- /dev/null +++ b/internal/logic/chat/list_rooms_logic.go @@ -0,0 +1,31 @@ +package chat + +import ( + "context" + + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ListRoomsLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 查詢聊天室列表 +func NewListRoomsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListRoomsLogic { + return &ListRoomsLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ListRoomsLogic) ListRooms(req *types.ListRoomsReq) (resp *types.ListRoomsResp, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/chat/remove_member_logic.go b/internal/logic/chat/remove_member_logic.go new file mode 100644 index 0000000..093d092 --- /dev/null +++ b/internal/logic/chat/remove_member_logic.go @@ -0,0 +1,31 @@ +package chat + +import ( + "context" + + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type RemoveMemberLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 移除聊天室成員 +func NewRemoveMemberLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RemoveMemberLogic { + return &RemoveMemberLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *RemoveMemberLogic) RemoveMember(req *types.RemoveMemberReq) (resp *types.RespOK, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/chat/send_message_logic.go b/internal/logic/chat/send_message_logic.go new file mode 100644 index 0000000..2e8c9ff --- /dev/null +++ b/internal/logic/chat/send_message_logic.go @@ -0,0 +1,31 @@ +package chat + +import ( + "context" + + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type SendMessageLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 發送訊息 +func NewSendMessageLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendMessageLogic { + return &SendMessageLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *SendMessageLogic) SendMessage(req *types.SendMessageReq) (resp *types.RespOK, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/chat/update_member_role_logic.go b/internal/logic/chat/update_member_role_logic.go new file mode 100644 index 0000000..0b2d321 --- /dev/null +++ b/internal/logic/chat/update_member_role_logic.go @@ -0,0 +1,31 @@ +package chat + +import ( + "context" + + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type UpdateMemberRoleLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 更新成員角色 +func NewUpdateMemberRoleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateMemberRoleLogic { + return &UpdateMemberRoleLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateMemberRoleLogic) UpdateMemberRole(req *types.UpdateMemberRoleReq) (resp *types.MemberResp, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/chat/update_room_logic.go b/internal/logic/chat/update_room_logic.go new file mode 100644 index 0000000..1611cab --- /dev/null +++ b/internal/logic/chat/update_room_logic.go @@ -0,0 +1,31 @@ +package chat + +import ( + "context" + + "backend/internal/svc" + "backend/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type UpdateRoomLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 更新聊天室資訊 +func NewUpdateRoomLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateRoomLogic { + return &UpdateRoomLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateRoomLogic) UpdateRoom(req *types.UpdateRoomReq) (resp *types.RoomResp, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/svc/chat.go b/internal/svc/chat.go index 0911de2..c3e387f 100644 --- a/internal/svc/chat.go +++ b/internal/svc/chat.go @@ -65,3 +65,7 @@ func initCassandraDB(c *config.Config) (*cassandra.DB, error) { return cassandra.New(opts...) } + +func MustRoomUseCase(c *config.Config, logger errs.Logger) usecase.R { + +} diff --git a/internal/types/types.go b/internal/types/types.go index 7e7c471..325c63b 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1,8 +1,14 @@ // Code generated by goctl. DO NOT EDIT. -// goctl 1.9.0 +// goctl 1.8.5 package types +type AddMemberReq struct { + Authorization + RoomID string `json:"room_id" validate:"required"` + UID string `json:"uid" validate:"required"` // 要添加的用戶 UID +} + type Authorization struct { Authorization string `header:"Authorization" validate:"required"` } @@ -10,12 +16,59 @@ type Authorization struct { type BaseReq struct { } +type CreateRoomReq struct { + Authorization + Name string `json:"name" validate:"required,min=1,max=100"` // 聊天室名稱 + Status string `json:"status,optional" validate:"omitempty,oneof=active archived"` // 狀態,預設為 active +} + type CredentialsPayload struct { Password string `json:"password" validate:"required,min=8,max=128"` // 密碼 (後端應使用 bcrypt 進行雜湊) PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password"` // 確認密碼 AccountType string `json:"account_type" validate:"required,oneof=email phone any"` // 帳號型別 email phone any } +type GetUserRoomsResp struct { + Rooms []RoomResp `json:"rooms"` + Total int64 `json:"total"` +} + +type IsMemberResp struct { + IsMember bool `json:"is_member"` +} + +type ListMembersResp struct { + Members []Member `json:"members"` + Total int64 `json:"total"` +} + +type ListMessagesReq struct { + Authorization + RoomID string `path:"room_id" validate:"required"` + BucketDay string `json:"bucket_day,optional"` // yyyyMMdd,不提供則使用今天 + PageSize int64 `json:"page_size,optional" validate:"omitempty,min=1,max=100"` // 每頁大小,預設 20 + LastTS int64 `json:"last_ts,optional"` // 用於 cursor-based pagination +} + +type ListMessagesResp struct { + Messages []MessageResp `json:"messages"` + Total int64 `json:"total,optional"` // 總數(僅第一頁返回) + LastTS int64 `json:"last_ts,optional"` // 用於下一頁查詢 +} + +type ListRoomsReq struct { + Authorization + Status string `json:"status,optional" validate:"omitempty,oneof=active archived"` // 狀態篩選 + PageSize int `json:"page_size,optional" validate:"omitempty,min=1,max=100"` // 每頁大小,預設 20 + LastID string `json:"last_id,optional"` // 用於 cursor-based pagination +} + +type ListRoomsResp struct { + Rooms []RoomResp `json:"rooms"` + LastID string `json:"last_id,optional"` // 用於下一頁查詢 + Total int64 `json:"total,optional"` // 總數(僅第一頁返回) +} + type LoginReq struct { AuthMethod string `json:"auth_method" validate:"required,oneof=credentials platform"` // 驗證類型 credentials platform LoginID string `json:"login_id" validate:"required,min=3,max=50"` // 信箱或手機號碼 @@ -30,6 +83,28 @@ type LoginResp struct { TokenType string `json:"token_type"` // 通常固定為 "Bearer" } +type Member struct { + UID string `json:"uid"` + Name string `json:"name"` + Avatar string `json:"avatar"` +} + +type MemberResp struct { + RoomID string `json:"room_id"` + UID string `json:"uid"` + Role string `json:"role"` + JoinedAt string `json:"joined_at"` + UpdatedAt string `json:"updated_at"` +} + +type MessageResp struct { + RoomID string `json:"room_id"` + BucketDay string `json:"bucket_day"` // yyyyMMdd + TS int64 `json:"ts"` // timestamp + UID string `json:"uid"` + Content string `json:"content"` +} + type MyInfo struct { Platform string `json:"platform"` // 註冊平台 UID string `json:"uid"` // 用戶 UID @@ -76,6 +151,12 @@ type RefreshTokenResp struct { TokenType string `json:"token_type"` } +type RemoveMemberReq struct { + Authorization + RoomID string `path:"room_id" validate:"required"` + UID string `path:"uid" validate:"required"` // 要移除的用戶 UID +} + type RequestPasswordResetReq struct { Identifier string `json:"identifier" validate:"required"` // 使用者帳號 (信箱或手機) AccountType string `json:"account_type" validate:"required,oneof=email phone"` @@ -104,6 +185,33 @@ type Resp struct { type RespOK struct { } +type RoomReq struct { + Authorization + RoomID string `path:"room_id"` +} + +type RoomResp struct { + RoomID string `json:"room_id"` + Name string `json:"name"` + Status string `json:"status"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +type SendMessageReq struct { + Authorization + RoomID string `path:"room_id" validate:"required"` + Content string `json:"content" validate:"required,min=1,max=5000"` // 訊息內容 +} + +type SendMessageResp struct { + RoomID string `json:"room_id"` + BucketDay string `json:"bucket_day"` // yyyyMMdd + TS int64 `json:"ts"` // timestamp + UID string `json:"uid"` + Content string `json:"content"` +} + type SubmitVerificationCodeReq struct { Account string `json:"account" validate:"required` Purpose string `json:"purpose" validate:"required,oneof=email_verification phone_verification"` @@ -111,6 +219,13 @@ type SubmitVerificationCodeReq struct { Authorization } +type UpdateMemberRoleReq struct { + Authorization + RoomID string `path:"room_id" validate:"required"` + UID string `path:"uid" validate:"required"` + Role string `json:"role" validate:"required,oneof=admin member"` +} + type UpdatePasswordReq struct { CurrentPassword string `json:"current_password" validate:"required"` NewPassword string `json:"new_password" validate:"required,min=8,max=128"` @@ -118,6 +233,13 @@ type UpdatePasswordReq struct { Authorization } +type UpdateRoomReq struct { + Authorization + RoomID string `path:"room_id" validate:"required"` + Name *string `json:"name,optional" validate:"omitempty,min=1,max=100"` + Status *string `json:"status,optional" validate:"omitempty,oneof=active archived"` +} + type UpdateUserInfoReq struct { AvatarURL *string `json:"avatar_url,optional"` // 頭像 URL FullName *string `json:"full_name,optional"` // 用戶全名 diff --git a/pkg/chat/domain/entity/message_dedup.go b/pkg/chat/domain/entity/message_dedup.go index 63f19d9..7e7cba7 100644 --- a/pkg/chat/domain/entity/message_dedup.go +++ b/pkg/chat/domain/entity/message_dedup.go @@ -8,9 +8,9 @@ import ( // Primary Key: ((room_id, uid), bucket_sec, content_md5) // TTL: 2 秒 type MessageDedup struct { - RoomID gocql.UUID `db:"room_id" partition_key:"true"` - UID string `db:"uid" partition_key:"true"` - BucketSec int64 `db:"bucket_sec" clustering_key:"true"` // Unix timestamp in seconds + RoomID gocql.UUID `db:"room_id" partition_key:"true"` + UID string `db:"uid" partition_key:"true"` + BucketSec int64 `db:"bucket_sec" clustering_key:"true"` // Unix timestamp in seconds ContentMD5 string `db:"content_md5" clustering_key:"true"` // MD5 hash of content } @@ -18,4 +18,3 @@ type MessageDedup struct { func (m MessageDedup) TableName() string { return "message_dedup" } - diff --git a/pkg/chat/domain/usecase/room.go b/pkg/chat/domain/usecase/room.go new file mode 100644 index 0000000..be47b76 --- /dev/null +++ b/pkg/chat/domain/usecase/room.go @@ -0,0 +1,118 @@ +package usecase + +import ( + "context" +) + +// RoomUseCase 定義聊天室相關的業務邏輯介面 +type RoomUseCase interface { + // CreateRoom 創建聊天室 + CreateRoom(ctx context.Context, req CreateRoomReq) (*Room, error) + // GetRoom 取得聊天室資訊 + GetRoom(ctx context.Context, req GetRoomReq) (*Room, error) + // UpdateRoom 更新聊天室資訊 + UpdateRoom(ctx context.Context, req UpdateRoomReq) (*Room, error) + // DeleteRoom 刪除聊天室 + DeleteRoom(ctx context.Context, req DeleteRoomReq) error + // ListRooms 查詢聊天室列表(分頁) + ListRooms(ctx context.Context, req ListRoomsReq) ([]Room, string, int64, error) + // IsUserInRoom 檢查用戶是否在聊天室中 + IsUserInRoom(ctx context.Context, req IsUserInRoomReq) (bool, error) + + // AddMember 添加成員到聊天室 + AddMember(ctx context.Context, req AddMemberReq) (*Member, error) + // RemoveMember 移除聊天室成員 + RemoveMember(ctx context.Context, req RemoveMemberReq) error + // UpdateMemberRole 更新成員角色 + UpdateMemberRole(ctx context.Context, req UpdateMemberRoleReq) (*Member, error) + // ListMembers 查詢聊天室成員列表 + ListMembers(ctx context.Context, req ListMembersReq) ([]Member, int64, error) + + // GetUserRooms 查詢用戶所在的聊天室 + GetUserRooms(ctx context.Context, req GetUserRoomsReq) ([]Room, int64, error) +} + +// ==================== 聊天室管理請求/回應 ==================== + +type CreateRoomReq struct { + UID string // 創建者 UID + Name string // 聊天室名稱 + Status string // 狀態,預設為 active +} + +type GetRoomReq struct { + UID string // 請求者 UID + RoomID string // 聊天室 ID +} + +type UpdateRoomReq struct { + UID string // 請求者 UID + RoomID string // 聊天室 ID + Name *string // 聊天室名稱(可選) + Status *string // 狀態(可選) +} + +type DeleteRoomReq struct { + UID string // 請求者 UID + RoomID string // 聊天室 ID +} + +type ListRoomsReq struct { + UID string // 用戶 UID(查詢該用戶所在的聊天室) + Status string // 狀態篩選(可選) + PageSize int // 每頁大小,預設 20 + LastID string // 用於 cursor-based pagination +} + +type IsUserInRoomReq struct { + UID string // 用戶 UID + RoomID string // 聊天室 ID +} + +type Room struct { + RoomID string `json:"room_id"` + Name string `json:"name"` + Status string `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +// ==================== 成員管理請求/回應 ==================== + +type AddMemberReq struct { + UID string // 操作者 UID + RoomID string // 聊天室 ID + MemberUID string // 要添加的用戶 UID + Role string // 角色,預設為 member +} + +type RemoveMemberReq struct { + UID string // 操作者 UID + RoomID string // 聊天室 ID + MemberUID string // 要移除的用戶 UID +} + +type UpdateMemberRoleReq struct { + UID string // 操作者 UID + RoomID string // 聊天室 ID + MemberUID string // 要更新的用戶 UID + Role string // 新角色 +} + +type ListMembersReq struct { + UID string // 請求者 UID + RoomID string // 聊天室 ID +} + +type Member struct { + RoomID string `json:"room_id"` + UID string `json:"uid"` + Role string `json:"role"` + JoinedAt int64 `json:"joined_at"` +} + +// ==================== 用戶相關請求/回應 ==================== + +type GetUserRoomsReq struct { + UID string // 用戶 UID +} diff --git a/pkg/chat/repository/message.go b/pkg/chat/repository/message.go index 8ea826f..e309d7c 100644 --- a/pkg/chat/repository/message.go +++ b/pkg/chat/repository/message.go @@ -20,7 +20,7 @@ type messageRepository struct { // MessageRepositoryParam 創建 MessageRepository 所需的參數 type MessageRepositoryParam struct { - DB *cassandra.DB + DB *cassandra.DB Keyspace string } diff --git a/pkg/chat/repository/room.go b/pkg/chat/repository/room.go index 68f1134..2785e9c 100644 --- a/pkg/chat/repository/room.go +++ b/pkg/chat/repository/room.go @@ -22,7 +22,7 @@ type roomRepository struct { // RoomRepositoryParam 創建 RoomRepository 所需的參數 type RoomRepositoryParam struct { - DB *cassandra.DB + DB *cassandra.DB Keyspace string } diff --git a/pkg/chat/repository/room_test.go b/pkg/chat/repository/room_test.go index b75b13c..0622ff5 100644 --- a/pkg/chat/repository/room_test.go +++ b/pkg/chat/repository/room_test.go @@ -182,7 +182,7 @@ func TestRoomRepository_RoomUpdate(t *testing.T) { require.NoError(t, err) assert.Equal(t, "Updated Name", updated.Name) assert.Equal(t, "archived", updated.Status) - assert.Equal(t, originalCreatedAt, updated.CreatedAt) // CreatedAt 不應該改變 + assert.Equal(t, originalCreatedAt, updated.CreatedAt) // CreatedAt 不應該改變 assert.Greater(t, updated.UpdatedAt, originalCreatedAt) // UpdatedAt 應該更新 } @@ -309,7 +309,7 @@ func TestRoomRepository_RoomCount(t *testing.T) { wantErr bool }{ { - name: "count all rooms", + name: "count all rooms", param: repository.CountRoomsReq{}, want: 4, }, @@ -950,4 +950,3 @@ func TestRoomRepository_Integration(t *testing.T) { assert.True(t, inRoom) } } - diff --git a/pkg/chat/usecase/room.go b/pkg/chat/usecase/room.go new file mode 100644 index 0000000..e6672ff --- /dev/null +++ b/pkg/chat/usecase/room.go @@ -0,0 +1,790 @@ +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) +} diff --git a/pkg/library/cassandra/db_test.go b/pkg/library/cassandra/db_test.go index 43db35c..1486508 100644 --- a/pkg/library/cassandra/db_test.go +++ b/pkg/library/cassandra/db_test.go @@ -542,4 +542,3 @@ func TestMultipleOptions(t *testing.T) { assert.True(t, cfg.UseAuth) }) } - diff --git a/pkg/library/cassandra/lock_test.go b/pkg/library/cassandra/lock_test.go index 736f657..693552d 100644 --- a/pkg/library/cassandra/lock_test.go +++ b/pkg/library/cassandra/lock_test.go @@ -10,9 +10,9 @@ import ( func TestWithLockTTL(t *testing.T) { tests := []struct { - name string - duration time.Duration - wantTTL int + name string + duration time.Duration + wantTTL int description string }{ { @@ -87,9 +87,9 @@ func TestWithNoLockExpire(t *testing.T) { func TestLockOptions_Combination(t *testing.T) { tests := []struct { - name string - opts []LockOption - wantTTL int + name string + opts []LockOption + wantTTL int }{ { name: "WithLockTTL then WithNoLockExpire", @@ -348,13 +348,13 @@ func TestLockOption_Type(t *testing.T) { func TestLockOptions_ApplyOrder(t *testing.T) { t.Run("last option should win", func(t *testing.T) { options := &lockOptions{ttlSeconds: defaultLockTTLSec} - + WithLockTTL(60 * time.Second)(options) assert.Equal(t, 60, options.ttlSeconds) - + WithNoLockExpire()(options) assert.Equal(t, 0, options.ttlSeconds) - + WithLockTTL(120 * time.Second)(options) assert.Equal(t, 120, options.ttlSeconds) }) @@ -500,4 +500,3 @@ func TestLockOptions_RealWorldScenarios(t *testing.T) { }) } } - diff --git a/pkg/library/cassandra/option_test.go b/pkg/library/cassandra/option_test.go index 788583d..fef0af4 100644 --- a/pkg/library/cassandra/option_test.go +++ b/pkg/library/cassandra/option_test.go @@ -162,59 +162,59 @@ func TestWithKeyspace(t *testing.T) { func TestWithAuth(t *testing.T) { tests := []struct { - name string - username string - password string - expectedUser string - expectedPass string + name string + username string + password string + expectedUser string + expectedPass string expectedUseAuth bool }{ { - name: "valid credentials", - username: "admin", - password: "password123", - expectedUser: "admin", - expectedPass: "password123", + name: "valid credentials", + username: "admin", + password: "password123", + expectedUser: "admin", + expectedPass: "password123", expectedUseAuth: true, }, { - name: "empty username", - username: "", - password: "password", - expectedUser: "", - expectedPass: "password", + name: "empty username", + username: "", + password: "password", + expectedUser: "", + expectedPass: "password", expectedUseAuth: true, }, { - name: "empty password", - username: "admin", - password: "", - expectedUser: "admin", - expectedPass: "", + name: "empty password", + username: "admin", + password: "", + expectedUser: "admin", + expectedPass: "", expectedUseAuth: true, }, { - name: "both empty", - username: "", - password: "", - expectedUser: "", - expectedPass: "", + name: "both empty", + username: "", + password: "", + expectedUser: "", + expectedPass: "", expectedUseAuth: true, }, { - name: "special characters in password", - username: "user", - password: "p@ssw0rd!#$%", - expectedUser: "user", - expectedPass: "p@ssw0rd!#$%", + name: "special characters in password", + username: "user", + password: "p@ssw0rd!#$%", + expectedUser: "user", + expectedPass: "p@ssw0rd!#$%", expectedUseAuth: true, }, { - name: "long username and password", - username: "very_long_username_that_might_exist", - password: "very_long_password_that_might_exist", - expectedUser: "very_long_username_that_might_exist", - expectedPass: "very_long_password_that_might_exist", + name: "long username and password", + username: "very_long_username_that_might_exist", + password: "very_long_password_that_might_exist", + expectedUser: "very_long_username_that_might_exist", + expectedPass: "very_long_password_that_might_exist", expectedUseAuth: true, }, } @@ -233,9 +233,9 @@ func TestWithAuth(t *testing.T) { func TestWithConsistency(t *testing.T) { tests := []struct { - name string - consistency gocql.Consistency - expected gocql.Consistency + name string + consistency gocql.Consistency + expected gocql.Consistency }{ { name: "Quorum consistency", @@ -387,7 +387,7 @@ func TestWithNumConns(t *testing.T) { func TestWithMaxRetries(t *testing.T) { tests := []struct { - name string + name string maxRetries int expected int }{ @@ -960,4 +960,3 @@ func TestOption_RealWorldScenarios(t *testing.T) { }) } } - diff --git a/pkg/library/cassandra/query_test.go b/pkg/library/cassandra/query_test.go index 1a11a87..9163efc 100644 --- a/pkg/library/cassandra/query_test.go +++ b/pkg/library/cassandra/query_test.go @@ -517,4 +517,3 @@ func TestQueryBuilder_Count_ErrorCases(t *testing.T) { }) } } - diff --git a/pkg/library/cassandra/repository_test.go b/pkg/library/cassandra/repository_test.go index fda023d..e772440 100644 --- a/pkg/library/cassandra/repository_test.go +++ b/pkg/library/cassandra/repository_test.go @@ -544,4 +544,3 @@ func TestBuildUpdateFields(t *testing.T) { }) } } - diff --git a/pkg/library/cassandra/sai.go b/pkg/library/cassandra/sai.go index dfec962..ffd6738 100644 --- a/pkg/library/cassandra/sai.go +++ b/pkg/library/cassandra/sai.go @@ -22,9 +22,9 @@ const ( // SAIIndexOptions 定義 SAI 索引選項 type SAIIndexOptions struct { - IndexType SAIIndexType // 索引類型 - IsAsync bool // 是否異步建立索引 - CaseSensitive bool // 是否區分大小寫(用於全文索引) + IndexType SAIIndexType // 索引類型 + IsAsync bool // 是否異步建立索引 + CaseSensitive bool // 是否區分大小寫(用於全文索引) } // DefaultSAIIndexOptions 返回預設的 SAI 索引選項 diff --git a/pkg/library/cassandra/types_test.go b/pkg/library/cassandra/types_test.go index f400523..b64eca9 100644 --- a/pkg/library/cassandra/types_test.go +++ b/pkg/library/cassandra/types_test.go @@ -137,4 +137,3 @@ func TestOrder_EdgeCases(t *testing.T) { }) } } - diff --git a/pkg/library/centrifugo/blacklist_test.go b/pkg/library/centrifugo/blacklist_test.go index 0b9f9ab..b0aaddc 100644 --- a/pkg/library/centrifugo/blacklist_test.go +++ b/pkg/library/centrifugo/blacklist_test.go @@ -243,4 +243,3 @@ func TestKeyGeneration(t *testing.T) { assert.Equal(t, "test:token:jti-123", blacklist.tokenKey("jti-123")) assert.Equal(t, "test:user_version:user-456", blacklist.userVersionKey("user-456")) } - diff --git a/pkg/library/centrifugo/centrifugo.go b/pkg/library/centrifugo/centrifugo.go index 3a459f8..7420249 100644 --- a/pkg/library/centrifugo/centrifugo.go +++ b/pkg/library/centrifugo/centrifugo.go @@ -185,4 +185,3 @@ func (s *Service) GetUsersOnlineStatus(ctx context.Context, userIDs []string) (m } return s.online.GetUsersOnlineStatus(ctx, userIDs) } - diff --git a/pkg/library/centrifugo/centrifugo_test.go b/pkg/library/centrifugo/centrifugo_test.go index 391a2a3..389e085 100644 --- a/pkg/library/centrifugo/centrifugo_test.go +++ b/pkg/library/centrifugo/centrifugo_test.go @@ -300,4 +300,3 @@ func TestService_MultipleConnections(t *testing.T) { require.NoError(t, err) assert.False(t, online) } - diff --git a/pkg/library/centrifugo/client.go b/pkg/library/centrifugo/client.go index b6bc8d9..b06f632 100644 --- a/pkg/library/centrifugo/client.go +++ b/pkg/library/centrifugo/client.go @@ -61,9 +61,9 @@ func HighPerformanceConfig(apiURL, apiKey string) ClientConfig { return ClientConfig{ APIURL: apiURL, APIKey: apiKey, - Timeout: 5 * time.Second, // 更短的超時 - MaxIdleConns: 200, // 更多閒置連線 - MaxIdleConnsPerHost: 50, // 更多每 host 連線 + Timeout: 5 * time.Second, // 更短的超時 + MaxIdleConns: 200, // 更多閒置連線 + MaxIdleConnsPerHost: 50, // 更多每 host 連線 IdleConnTimeout: 120 * time.Second, DialTimeout: 3 * time.Second, TLSHandshakeTimeout: 3 * time.Second, @@ -368,7 +368,6 @@ func (c *Client) DisconnectWithCode(ctx context.Context, user string, code uint3 return c.callAPI(ctx, "disconnect", req, nil) } - // ==================== 在線狀態方法 ==================== // Presence 獲取頻道在線用戶 diff --git a/pkg/library/centrifugo/online_redis_test.go b/pkg/library/centrifugo/online_redis_test.go index 8cabae3..305d48e 100644 --- a/pkg/library/centrifugo/online_redis_test.go +++ b/pkg/library/centrifugo/online_redis_test.go @@ -269,4 +269,3 @@ func TestOnlineStore_ImplementsInterface(t *testing.T) { // 確保 RedisOnlineStore 實現了 OnlineStore 介面 var _ OnlineStore = store } - diff --git a/pkg/notification/domain/notification/notification_status.go b/pkg/notification/domain/notification/notification_status.go index 8e132f7..efa0e1a 100644 --- a/pkg/notification/domain/notification/notification_status.go +++ b/pkg/notification/domain/notification/notification_status.go @@ -7,7 +7,7 @@ func (n NotifyStatus) ToString() string { if !ok { return "unknown" } - + return status } diff --git a/pkg/utils/time.go b/pkg/utils/time.go index d0a4841..6401497 100644 --- a/pkg/utils/time.go +++ b/pkg/utils/time.go @@ -11,4 +11,3 @@ func GetBucketDay(t time.Time) string { func GetTodayBucketDay() string { return GetBucketDay(time.Now()) } -