From c354618e2004407f875f46db53be6f97cecd1508 Mon Sep 17 00:00:00 2001 From: wxy Date: Fri, 13 Mar 2026 15:57:23 +0800 Subject: [PATCH 1/4] feat(app): add shared_only filter and batch unshare endpoints --- api/app/controllers/app_controller.py | 36 ++++++++++++ api/app/services/app_service.py | 80 ++++++++++++++++++++++++++- 2 files changed, 114 insertions(+), 2 deletions(-) diff --git a/api/app/controllers/app_controller.py b/api/app/controllers/app_controller.py index 5d1551d8..73ff7279 100644 --- a/api/app/controllers/app_controller.py +++ b/api/app/controllers/app_controller.py @@ -53,6 +53,7 @@ def list_apps( status: str | None = None, search: str | None = None, include_shared: bool = True, + shared_only: bool = False, page: int = 1, pagesize: int = 10, ids: Optional[str] = None, @@ -84,6 +85,7 @@ def list_apps( status=status, search=search, include_shared=include_shared, + shared_only=shared_only, page=page, pagesize=pagesize, ) @@ -107,6 +109,23 @@ def list_my_shared_out( return success(data=data) +@router.delete("/share/{target_workspace_id}", summary="取消对某工作空间的所有应用分享") +@cur_workspace_access_guard() +def unshare_all_apps_to_workspace( + target_workspace_id: uuid.UUID, + db: Session = Depends(get_db), + current_user=Depends(get_current_user), +): + """Cancel all app shares from current workspace to a target workspace.""" + workspace_id = current_user.current_workspace_id + service = app_service.AppService(db) + count = service.unshare_all_apps_to_workspace( + target_workspace_id=target_workspace_id, + workspace_id=workspace_id + ) + return success(msg=f"已取消 {count} 个应用的分享") + + @router.get("/{app_id}", summary="获取应用详情") @cur_workspace_access_guard() def get_app( @@ -397,6 +416,23 @@ def list_app_shares( return success(data=data) +@router.delete("/shared/{source_workspace_id}", summary="批量移除某来源工作空间的所有共享应用") +@cur_workspace_access_guard() +def remove_all_shared_apps_from_workspace( + source_workspace_id: uuid.UUID, + db: Session = Depends(get_db), + current_user=Depends(get_current_user), +): + """Remove all shared apps from a specific source workspace (recipient operation).""" + workspace_id = current_user.current_workspace_id + service = app_service.AppService(db) + count = service.remove_all_shared_apps_from_workspace( + source_workspace_id=source_workspace_id, + workspace_id=workspace_id + ) + return success(msg=f"已移除 {count} 个共享应用") + + @router.delete("/{app_id}/shared", summary="移除共享给我的应用") @cur_workspace_access_guard() def remove_shared_app( diff --git a/api/app/services/app_service.py b/api/app/services/app_service.py index 2326be6e..d83f88d5 100644 --- a/api/app/services/app_service.py +++ b/api/app/services/app_service.py @@ -12,7 +12,7 @@ import uuid from typing import Annotated, Any, Dict, List, Optional, Tuple from fastapi import Depends -from sqlalchemy import and_, func, or_, select +from sqlalchemy import and_, delete, func, or_, select from sqlalchemy.orm import Session from app.core.error_codes import BizCode @@ -878,6 +878,7 @@ class AppService: status: Optional[str] = None, search: Optional[str] = None, include_shared: bool = True, + shared_only: bool = False, page: int = 1, pagesize: int = 10, ) -> Tuple[List[App], int]: @@ -924,7 +925,14 @@ class AppService: filters.append(func.lower(App.name).like(f"%{search.lower()}%")) # 基础查询:本工作空间的应用 - if include_shared: + if shared_only: + # 只返回共享给本工作空间的应用,不含自有应用 + shared_app_ids_stmt = ( + select(AppShare.source_app_id) + .where(AppShare.target_workspace_id == workspace_id) + ) + stmt = select(App).where(App.id.in_(shared_app_ids_stmt)) + elif include_shared: # 查询本工作空间的应用 + 分享给本工作空间的应用 # 使用 OR 条件:workspace_id = current OR app_id IN (shared apps) @@ -1891,6 +1899,39 @@ class AppService: extra={"app_id": str(app_id), "target_workspace_id": str(target_workspace_id)} ) + def unshare_all_apps_to_workspace( + self, + *, + target_workspace_id: uuid.UUID, + workspace_id: uuid.UUID + ) -> int: + """Cancel all app shares from current workspace to a target workspace. + + Args: + target_workspace_id: Target workspace ID to cancel all shares to + workspace_id: Current workspace ID (source) + + Returns: + Number of share records deleted + """ + from app.models import AppShare + + logger.info( + "取消对目标工作空间的所有应用分享", + extra={"target_workspace_id": str(target_workspace_id), "workspace_id": str(workspace_id)} + ) + + stmt = delete(AppShare).where( + AppShare.source_workspace_id == workspace_id, + AppShare.target_workspace_id == target_workspace_id + ) + result = self.db.execute(stmt) + self.db.commit() + + count = result.rowcount + logger.info("已取消分享记录数", extra={"count": count}) + return count + def list_app_shares( self, *, @@ -1976,6 +2017,39 @@ class AppService: extra={"app_id": str(app_id), "workspace_id": str(workspace_id)} ) + def remove_all_shared_apps_from_workspace( + self, + *, + source_workspace_id: uuid.UUID, + workspace_id: uuid.UUID + ) -> int: + """Remove all shared apps from a specific source workspace. + + Args: + source_workspace_id: The workspace that shared the apps + workspace_id: Current workspace ID (recipient) + + Returns: + Number of share records deleted + """ + from app.models import AppShare + + logger.info( + "批量移除来源工作空间的共享应用", + extra={"source_workspace_id": str(source_workspace_id), "workspace_id": str(workspace_id)} + ) + + stmt = delete(AppShare).where( + AppShare.source_workspace_id == source_workspace_id, + AppShare.target_workspace_id == workspace_id + ) + result = self.db.execute(stmt) + self.db.commit() + + count = result.rowcount + logger.info("已移除共享记录数", extra={"count": count}) + return count + def list_my_shared_out( self, *, @@ -2139,6 +2213,7 @@ def list_apps( status: Optional[str] = None, search: Optional[str] = None, include_shared: bool = True, + shared_only: bool = False, page: int = 1, pagesize: int = 10, ) -> Tuple[List[App], int]: @@ -2151,6 +2226,7 @@ def list_apps( status=status, search=search, include_shared=include_shared, + shared_only=shared_only, page=page, pagesize=pagesize, ) From aab54ca1a8d6c21bfb99a341233f2c461a63a590 Mon Sep 17 00:00:00 2001 From: wxy Date: Fri, 13 Mar 2026 16:19:35 +0800 Subject: [PATCH 2/4] refactor(app): address AI review suggestions on sharing endpoints --- api/app/controllers/app_controller.py | 4 ++-- api/app/services/app_service.py | 28 +++++++++++++++++++-------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/api/app/controllers/app_controller.py b/api/app/controllers/app_controller.py index 73ff7279..63f484ca 100644 --- a/api/app/controllers/app_controller.py +++ b/api/app/controllers/app_controller.py @@ -123,7 +123,7 @@ def unshare_all_apps_to_workspace( target_workspace_id=target_workspace_id, workspace_id=workspace_id ) - return success(msg=f"已取消 {count} 个应用的分享") + return success(msg=f"已取消 {count} 个应用的分享", data={"count": count}) @router.get("/{app_id}", summary="获取应用详情") @@ -430,7 +430,7 @@ def remove_all_shared_apps_from_workspace( source_workspace_id=source_workspace_id, workspace_id=workspace_id ) - return success(msg=f"已移除 {count} 个共享应用") + return success(msg=f"已移除 {count} 个共享应用", data={"count": count}) @router.delete("/{app_id}/shared", summary="移除共享给我的应用") diff --git a/api/app/services/app_service.py b/api/app/services/app_service.py index d83f88d5..ca9a3c4a 100644 --- a/api/app/services/app_service.py +++ b/api/app/services/app_service.py @@ -924,6 +924,10 @@ class AppService: if search: filters.append(func.lower(App.name).like(f"%{search.lower()}%")) + # shared_only implies include_shared; enforce to avoid confusing API usage + if shared_only: + include_shared = True + # 基础查询:本工作空间的应用 if shared_only: # 只返回共享给本工作空间的应用,不含自有应用 @@ -1921,14 +1925,18 @@ class AppService: extra={"target_workspace_id": str(target_workspace_id), "workspace_id": str(workspace_id)} ) - stmt = delete(AppShare).where( + # Query IDs first to get a reliable count, avoiding rowcount driver inconsistencies + id_stmt = select(AppShare.id).where( AppShare.source_workspace_id == workspace_id, AppShare.target_workspace_id == target_workspace_id ) - result = self.db.execute(stmt) - self.db.commit() + ids = list(self.db.scalars(id_stmt).all()) + count = len(ids) + + if ids: + self.db.execute(delete(AppShare).where(AppShare.id.in_(ids))) + self.db.commit() - count = result.rowcount logger.info("已取消分享记录数", extra={"count": count}) return count @@ -2039,14 +2047,18 @@ class AppService: extra={"source_workspace_id": str(source_workspace_id), "workspace_id": str(workspace_id)} ) - stmt = delete(AppShare).where( + # Query IDs first to get a reliable count, avoiding rowcount driver inconsistencies + id_stmt = select(AppShare.id).where( AppShare.source_workspace_id == source_workspace_id, AppShare.target_workspace_id == workspace_id ) - result = self.db.execute(stmt) - self.db.commit() + ids = list(self.db.scalars(id_stmt).all()) + count = len(ids) + + if ids: + self.db.execute(delete(AppShare).where(AppShare.id.in_(ids))) + self.db.commit() - count = result.rowcount logger.info("已移除共享记录数", extra={"count": count}) return count From c7cc0cd92213babd9bbf984d140da0f596ec6b0c Mon Sep 17 00:00:00 2001 From: wxy Date: Fri, 13 Mar 2026 17:02:29 +0800 Subject: [PATCH 3/4] refactor(app): use soft delete for app shares via is_active flag --- api/app/models/appshare_model.py | 3 +- api/app/services/app_service.py | 66 ++++++++++++++++++++------------ 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/api/app/models/appshare_model.py b/api/app/models/appshare_model.py index 643b0e11..6902051a 100644 --- a/api/app/models/appshare_model.py +++ b/api/app/models/appshare_model.py @@ -1,6 +1,6 @@ import datetime import uuid -from sqlalchemy import Column, DateTime, ForeignKey, String +from sqlalchemy import Boolean, Column, DateTime, ForeignKey, String from sqlalchemy.dialects.postgresql import UUID from app.db import Base from sqlalchemy.orm import relationship @@ -19,6 +19,7 @@ class AppShare(Base): target_workspace_id = Column(UUID(as_uuid=True), ForeignKey('workspaces.id'), nullable=False, comment="目标工作空间ID") shared_by = Column(UUID(as_uuid=True), ForeignKey('users.id'), nullable=False, comment="分享者用户ID") permission = Column(String, default="readonly", nullable=False, comment="权限模式: readonly | editable") + is_active = Column(Boolean, default=True, nullable=False, comment="是否有效,False 表示逻辑删除") created_at = Column(DateTime, default=datetime.datetime.now) updated_at = Column(DateTime, default=datetime.datetime.now) diff --git a/api/app/services/app_service.py b/api/app/services/app_service.py index ca9a3c4a..c06d79a9 100644 --- a/api/app/services/app_service.py +++ b/api/app/services/app_service.py @@ -102,7 +102,8 @@ class AppService: # 2. 检查是否是共享给本工作空间的应用 stmt = select(AppShare).where( AppShare.source_app_id == app.id, - AppShare.target_workspace_id == workspace_id + AppShare.target_workspace_id == workspace_id, + AppShare.is_active.is_(True) ) share = self.db.scalars(stmt).first() @@ -140,7 +141,8 @@ class AppService: stmt = select(AppShare).where( AppShare.source_app_id == app.id, - AppShare.target_workspace_id == workspace_id + AppShare.target_workspace_id == workspace_id, + AppShare.is_active.is_(True) ) share = self.db.scalars(stmt).first() return share.permission if share else None @@ -509,7 +511,8 @@ class AppService: from app.models import AppShare stmt = select(AppShare).where( AppShare.source_app_id == app.id, - AppShare.target_workspace_id == current_workspace_id + AppShare.target_workspace_id == current_workspace_id, + AppShare.is_active.is_(True) ) share = self.db.scalars(stmt).first() if share: @@ -933,20 +936,15 @@ class AppService: # 只返回共享给本工作空间的应用,不含自有应用 shared_app_ids_stmt = ( select(AppShare.source_app_id) - .where(AppShare.target_workspace_id == workspace_id) + .where(AppShare.target_workspace_id == workspace_id, AppShare.is_active.is_(True)) ) stmt = select(App).where(App.id.in_(shared_app_ids_stmt)) elif include_shared: # 查询本工作空间的应用 + 分享给本工作空间的应用 - # 使用 OR 条件:workspace_id = current OR app_id IN (shared apps) - - # 获取分享给本工作空间的应用ID列表 shared_app_ids_stmt = ( select(AppShare.source_app_id) - .where(AppShare.target_workspace_id == workspace_id) + .where(AppShare.target_workspace_id == workspace_id, AppShare.is_active.is_(True)) ) - - # 构建主查询:本工作空间的应用 OR 分享的应用 stmt = select(App).where( or_( App.workspace_id == workspace_id, @@ -1801,7 +1799,8 @@ class AppService: # 检查是否已经分享过 stmt = select(AppShare).where( AppShare.source_app_id == app_id, - AppShare.target_workspace_id == target_ws_id + AppShare.target_workspace_id == target_ws_id, + AppShare.is_active.is_(True) ) existing_share = self.db.scalars(stmt).first() @@ -1880,7 +1879,8 @@ class AppService: # 2. 查找分享记录 stmt = select(AppShare).where( AppShare.source_app_id == app_id, - AppShare.target_workspace_id == target_workspace_id + AppShare.target_workspace_id == target_workspace_id, + AppShare.is_active.is_(True) ) share = self.db.scalars(stmt).first() @@ -1894,8 +1894,8 @@ class AppService: f"app_id={app_id}, target_workspace_id={target_workspace_id}" ) - # 3. 删除分享记录 - self.db.delete(share) + # 3. 逻辑删除分享记录 + share.is_active = False self.db.commit() logger.info( @@ -1925,16 +1925,21 @@ class AppService: extra={"target_workspace_id": str(target_workspace_id), "workspace_id": str(workspace_id)} ) - # Query IDs first to get a reliable count, avoiding rowcount driver inconsistencies + # Query active records first for reliable count id_stmt = select(AppShare.id).where( AppShare.source_workspace_id == workspace_id, - AppShare.target_workspace_id == target_workspace_id + AppShare.target_workspace_id == target_workspace_id, + AppShare.is_active.is_(True) ) ids = list(self.db.scalars(id_stmt).all()) count = len(ids) if ids: - self.db.execute(delete(AppShare).where(AppShare.id.in_(ids))) + # Soft delete: mark as inactive + from sqlalchemy import update as sa_update + self.db.execute( + sa_update(AppShare).where(AppShare.id.in_(ids)).values(is_active=False) + ) self.db.commit() logger.info("已取消分享记录数", extra={"count": count}) @@ -1969,7 +1974,8 @@ class AppService: # 查询分享记录 stmt = select(AppShare).where( - AppShare.source_app_id == app_id + AppShare.source_app_id == app_id, + AppShare.is_active.is_(True) ).order_by(AppShare.created_at.desc()) shares = list(self.db.scalars(stmt).all()) @@ -2007,7 +2013,8 @@ class AppService: stmt = select(AppShare).where( AppShare.source_app_id == app_id, - AppShare.target_workspace_id == workspace_id + AppShare.target_workspace_id == workspace_id, + AppShare.is_active.is_(True) ) share = self.db.scalars(stmt).first() @@ -2017,7 +2024,8 @@ class AppService: f"app_id={app_id}, workspace_id={workspace_id}" ) - self.db.delete(share) + # Soft delete + share.is_active = False self.db.commit() logger.info( @@ -2047,16 +2055,20 @@ class AppService: extra={"source_workspace_id": str(source_workspace_id), "workspace_id": str(workspace_id)} ) - # Query IDs first to get a reliable count, avoiding rowcount driver inconsistencies + # Query active records for reliable count, then soft delete id_stmt = select(AppShare.id).where( AppShare.source_workspace_id == source_workspace_id, - AppShare.target_workspace_id == workspace_id + AppShare.target_workspace_id == workspace_id, + AppShare.is_active.is_(True) ) ids = list(self.db.scalars(id_stmt).all()) count = len(ids) if ids: - self.db.execute(delete(AppShare).where(AppShare.id.in_(ids))) + from sqlalchemy import update as sa_update + self.db.execute( + sa_update(AppShare).where(AppShare.id.in_(ids)).values(is_active=False) + ) self.db.commit() logger.info("已移除共享记录数", extra={"count": count}) @@ -2076,7 +2088,10 @@ class AppService: stmt = ( select(AppShare) - .where(AppShare.source_workspace_id == workspace_id) + .where( + AppShare.source_workspace_id == workspace_id, + AppShare.is_active.is_(True) + ) .order_by(AppShare.created_at.desc()) ) return list(self.db.scalars(stmt).all()) @@ -2109,7 +2124,8 @@ class AppService: stmt = select(AppShare).where( AppShare.source_app_id == app_id, - AppShare.target_workspace_id == target_workspace_id + AppShare.target_workspace_id == target_workspace_id, + AppShare.is_active.is_(True) ) share = self.db.scalars(stmt).first() From 6eb9b772e74c0b787e0632f303ead032e2752916 Mon Sep 17 00:00:00 2001 From: wxy Date: Fri, 13 Mar 2026 17:28:27 +0800 Subject: [PATCH 4/4] feat(app): add is_active to app_shares with migration --- api/app/models/appshare_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/app/models/appshare_model.py b/api/app/models/appshare_model.py index 6902051a..4e9312d6 100644 --- a/api/app/models/appshare_model.py +++ b/api/app/models/appshare_model.py @@ -19,7 +19,7 @@ class AppShare(Base): target_workspace_id = Column(UUID(as_uuid=True), ForeignKey('workspaces.id'), nullable=False, comment="目标工作空间ID") shared_by = Column(UUID(as_uuid=True), ForeignKey('users.id'), nullable=False, comment="分享者用户ID") permission = Column(String, default="readonly", nullable=False, comment="权限模式: readonly | editable") - is_active = Column(Boolean, default=True, nullable=False, comment="是否有效,False 表示逻辑删除") + is_active = Column(Boolean, default=True, server_default='true', nullable=False, comment="是否有效,False 表示逻辑删除") created_at = Column(DateTime, default=datetime.datetime.now) updated_at = Column(DateTime, default=datetime.datetime.now)