diff --git a/packages/adapters/postgres/__init__.py b/packages/adapters/postgres/__init__.py new file mode 100644 index 000000000..598c1f8dd --- /dev/null +++ b/packages/adapters/postgres/__init__.py @@ -0,0 +1,12 @@ +""" +PostgreSQL 适配器 +""" +from packages.adapters.postgres.user_repository import PostgresUserRepository +from packages.adapters.postgres.workspace_repository import PostgresWorkspaceRepository +from packages.adapters.postgres.workspace_member_repository import PostgresWorkspaceMemberRepository + +__all__ = [ + "PostgresUserRepository", + "PostgresWorkspaceRepository", + "PostgresWorkspaceMemberRepository", +] diff --git a/packages/adapters/postgres/workspace_member_repository.py b/packages/adapters/postgres/workspace_member_repository.py new file mode 100644 index 000000000..d4d696436 --- /dev/null +++ b/packages/adapters/postgres/workspace_member_repository.py @@ -0,0 +1,138 @@ +""" +PostgreSQL WorkspaceMember Repository 实现 +""" +from typing import Optional, List +import psycopg2 +from psycopg2.extras import RealDictCursor + +from packages.domain.entities import WorkspaceMember +from packages.ports.workspace_member_repository import WorkspaceMemberRepository + + +class PostgresWorkspaceMemberRepository(WorkspaceMemberRepository): + """WorkspaceMember 仓储 PostgreSQL 实现""" + + def __init__(self, connection_string: str): + self.connection_string = connection_string + + def _get_connection(self): + return psycopg2.connect(self.connection_string, cursor_factory=RealDictCursor) + + def save(self, member: WorkspaceMember) -> None: + """保存成员""" + conn = self._get_connection() + try: + with conn.cursor() as cur: + cur.execute(""" + INSERT INTO workspace_members ( + id, workspace_id, user_id, role, invited_by, joined_at + ) VALUES ( + %(id)s, %(workspace_id)s, %(user_id)s, %(role)s, + %(invited_by)s, %(joined_at)s + ) + ON CONFLICT (workspace_id, user_id) DO UPDATE SET + role = EXCLUDED.role + """, { + "id": member.id, + "workspace_id": member.workspace_id, + "user_id": member.user_id, + "role": member.role, + "invited_by": member.invited_by, + "joined_at": member.joined_at, + }) + conn.commit() + finally: + conn.close() + + def find_by_id(self, member_id: str) -> Optional[WorkspaceMember]: + """根据 ID 查找成员""" + conn = self._get_connection() + try: + with conn.cursor() as cur: + cur.execute("SELECT * FROM workspace_members WHERE id = %s", (member_id,)) + row = cur.fetchone() + return self._row_to_member(row) if row else None + finally: + conn.close() + + def find_by_workspace_and_user( + self, + workspace_id: str, + user_id: str, + ) -> Optional[WorkspaceMember]: + """根据 workspace 和 user 查找成员""" + conn = self._get_connection() + try: + with conn.cursor() as cur: + cur.execute( + "SELECT * FROM workspace_members WHERE workspace_id = %s AND user_id = %s", + (workspace_id, user_id) + ) + row = cur.fetchone() + return self._row_to_member(row) if row else None + finally: + conn.close() + + def find_by_user(self, user_id: str) -> List[WorkspaceMember]: + """查找用户的所有成员记录""" + conn = self._get_connection() + try: + with conn.cursor() as cur: + cur.execute( + "SELECT * FROM workspace_members WHERE user_id = %s ORDER BY joined_at DESC", + (user_id,) + ) + rows = cur.fetchall() + return [self._row_to_member(row) for row in rows] + finally: + conn.close() + + def find_by_workspace(self, workspace_id: str) -> List[WorkspaceMember]: + """查找 workspace 的所有成员""" + conn = self._get_connection() + try: + with conn.cursor() as cur: + cur.execute( + "SELECT * FROM workspace_members WHERE workspace_id = %s ORDER BY joined_at", + (workspace_id,) + ) + rows = cur.fetchall() + return [self._row_to_member(row) for row in rows] + finally: + conn.close() + + def count_by_workspace(self, workspace_id: str) -> int: + """统计 workspace 的成员数量""" + conn = self._get_connection() + try: + with conn.cursor() as cur: + cur.execute( + "SELECT COUNT(*) FROM workspace_members WHERE workspace_id = %s", + (workspace_id,) + ) + return cur.fetchone()["count"] + finally: + conn.close() + + def delete(self, member_id: str) -> bool: + """删除成员""" + conn = self._get_connection() + try: + with conn.cursor() as cur: + cur.execute("DELETE FROM workspace_members WHERE id = %s", (member_id,)) + deleted = cur.rowcount > 0 + conn.commit() + return deleted + finally: + conn.close() + + def _row_to_member(self, row: dict) -> WorkspaceMember: + """将数据库行转换为 WorkspaceMember 对象""" + return WorkspaceMember( + id=row["id"], + workspace_id=row["workspace_id"], + user_id=row["user_id"], + role=row["role"], + invited_by=row["invited_by"], + joined_at=row["joined_at"], + ) diff --git a/packages/adapters/postgres/workspace_repository.py b/packages/adapters/postgres/workspace_repository.py new file mode 100644 index 000000000..967793d5c --- /dev/null +++ b/packages/adapters/postgres/workspace_repository.py @@ -0,0 +1,100 @@ +""" +PostgreSQL Workspace Repository 实现 +""" +from typing import Optional +import psycopg2 +from psycopg2.extras import RealDictCursor + +from packages.domain.entities import Workspace +from packages.ports.workspace_repository import WorkspaceRepository + + +class PostgresWorkspaceRepository(WorkspaceRepository): + """Workspace 仓储 PostgreSQL 实现""" + + def __init__(self, connection_string: str): + self.connection_string = connection_string + + def _get_connection(self): + """获取数据库连接""" + return psycopg2.connect(self.connection_string, cursor_factory=RealDictCursor) + + def save(self, workspace: Workspace) -> None: + """保存工作空间""" + conn = self._get_connection() + try: + with conn.cursor() as cur: + cur.execute(""" + INSERT INTO workspaces ( + id, name, owner_user_id, subscription_plan, + subscription_status, subscription_expires_at, + max_projects, max_storage_gb, used_storage_gb, created_at + ) VALUES ( + %(id)s, %(name)s, %(owner_user_id)s, %(subscription_plan)s, + %(subscription_status)s, %(subscription_expires_at)s, + %(max_projects)s, %(max_storage_gb)s, %(used_storage_gb)s, %(created_at)s + ) + ON CONFLICT (id) DO UPDATE SET + name = EXCLUDED.name, + subscription_plan = EXCLUDED.subscription_plan, + subscription_status = EXCLUDED.subscription_status, + subscription_expires_at = EXCLUDED.subscription_expires_at, + max_projects = EXCLUDED.max_projects, + max_storage_gb = EXCLUDED.max_storage_gb, + used_storage_gb = EXCLUDED.used_storage_gb + """, { + "id": workspace.id, + "name": workspace.name, + "owner_user_id": workspace.owner_user_id, + "subscription_plan": workspace.subscription_plan, + "subscription_status": workspace.subscription_status, + "subscription_expires_at": workspace.subscription_expires_at, + "max_projects": workspace.max_projects, + "max_storage_gb": workspace.max_storage_gb, + "used_storage_gb": workspace.used_storage_gb, + "created_at": workspace.created_at, + }) + conn.commit() + finally: + conn.close() + + def find_by_id(self, workspace_id: str) -> Optional[Workspace]: + """根据 ID 查找工作空间""" + conn = self._get_connection() + try: + with conn.cursor() as cur: + cur.execute("SELECT * FROM workspaces WHERE id = %s", (workspace_id,)) + row = cur.fetchone() + + if row: + return self._row_to_workspace(row) + return None + finally: + conn.close() + + def delete(self, workspace_id: str) -> bool: + """删除工作空间""" + conn = self._get_connection() + try: + with conn.cursor() as cur: + cur.execute("DELETE FROM workspaces WHERE id = %s", (workspace_id,)) + deleted = cur.rowcount > 0 + conn.commit() + return deleted + finally: + conn.close() + + def _row_to_workspace(self, row: dict) -> Workspace: + """将数据库行转换为 Workspace 对象""" + return Workspace( + id=row["id"], + name=row["name"], + owner_user_id=row["owner_user_id"], + subscription_plan=row["subscription_plan"], + subscription_status=row["subscription_status"], + subscription_expires_at=row["subscription_expires_at"], + max_projects=row["max_projects"], + max_storage_gb=row["max_storage_gb"], + used_storage_gb=float(row["used_storage_gb"]), + created_at=row["created_at"], + )