From 8d0c4c873fa468cf8e82b47edbafd795b092fbff Mon Sep 17 00:00:00 2001 From: CI Bot Date: Sun, 19 Jul 2026 09:17:59 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(P1):=20=E6=88=90=E7=89=87=E5=BA=93?= =?UTF-8?q?=E6=8C=89=E7=94=A8=E6=88=B7=E7=BB=B4=E5=BA=A6=E6=94=B9=E9=80=A0?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8D=E4=B8=80=E9=94=AE=E7=94=9F=E6=88=90?= =?UTF-8?q?=E6=9F=A5=E4=B8=8D=E5=88=B0+=E8=B6=8A=E6=9D=83=E6=BC=8F?= =?UTF-8?q?=E6=B4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - generated_videos表加user_id列及索引 - 写表时透传user_id(edit_plan_generation + one_take生成) - /videos接口默认按当前用户查,不传project_id不再查全表 - project_id降级为可选过滤条件 - 补单元测试覆盖user_id过滤逻辑 --- .../044_add_user_id_to_generated_videos.py | 34 +++++ apps/api/app/api/routes/videos.py | 5 +- apps/worker/video_processing/dedup_helpers.py | 2 + .../worker_app/tasks/edit_plan_generation.py | 1 + apps/worker/worker_app/tasks/generation.py | 3 + .../generated_video_repository.py | 8 +- packages/adapters/sqlalchemy_impl/models.py | 1 + packages/application/generated_videos.py | 2 + packages/domain/generated_video.py | 3 + packages/ports/generated_video_repository.py | 1 + .../test_generated_video_creation_logic.py | 129 ++++++++++++++++++ 11 files changed, 186 insertions(+), 3 deletions(-) create mode 100755 alembic/versions/044_add_user_id_to_generated_videos.py mode change 100644 => 100755 apps/api/app/api/routes/videos.py mode change 100644 => 100755 packages/domain/generated_video.py mode change 100644 => 100755 tests/unit/test_generated_video_creation_logic.py diff --git a/alembic/versions/044_add_user_id_to_generated_videos.py b/alembic/versions/044_add_user_id_to_generated_videos.py new file mode 100755 index 000000000..fec35c85b --- /dev/null +++ b/alembic/versions/044_add_user_id_to_generated_videos.py @@ -0,0 +1,34 @@ +"""add user_id to generated_videos + +Revision ID: 044_user_id_generated_videos +Revises: 043_updated_at_generation_tasks +Create Date: 2026-07-19 08:00:00.000000 + +""" + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "044_user_id_generated_videos" +down_revision = "043_updated_at_generation_tasks" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + "generated_videos", + sa.Column( + "user_id", + sa.String(36), + nullable=False, + server_default="", + index=True, + ), + ) + + +def downgrade() -> None: + op.drop_column("generated_videos", "user_id") diff --git a/apps/api/app/api/routes/videos.py b/apps/api/app/api/routes/videos.py old mode 100644 new mode 100755 index 9fb7e6b74..e8a0b52bd --- a/apps/api/app/api/routes/videos.py +++ b/apps/api/app/api/routes/videos.py @@ -57,7 +57,7 @@ def _to_video_response(item, storage: OSSStorageService | None = None) -> VideoI @router.get("/videos", response_model=ListVideosResponse) def list_videos( - project_id: str | None = Query(None, description="项目ID,不传则返回所有项目"), + project_id: str | None = Query(None, description="项目ID,可选过滤"), status: str | None = Query(None, description="按状态筛选"), review_status: str | None = Query(None, description="按复核状态筛选"), page: int = Query(1, ge=1, description="页码"), @@ -66,9 +66,10 @@ def list_videos( storage: OSSStorageService = Depends(get_storage_service), current_user: AuthenticatedUser = Depends(get_current_user), ): - """成片列表,支持分页、按项目/状态/复核状态筛选。""" + """成片列表,默认返回当前用户的所有成片,支持按项目/状态/复核状态筛选。""" use_case = ListGeneratedVideosPaginatedUseCase(repo) items, total = use_case.execute( + user_id=current_user.user.id, project_id=project_id, status=status, review_status=review_status, diff --git a/apps/worker/video_processing/dedup_helpers.py b/apps/worker/video_processing/dedup_helpers.py index e67a94c3a..c074bc3b0 100755 --- a/apps/worker/video_processing/dedup_helpers.py +++ b/apps/worker/video_processing/dedup_helpers.py @@ -18,6 +18,7 @@ def create_video_record_and_dedup( *, generation_task_id: str, project_id: str, + user_id: str = "", batch_id: str, file_url: str, file_size: int, @@ -63,6 +64,7 @@ def create_video_record_and_dedup( generated_video = GeneratedVideo( id=video_id, project_id=project_id, + user_id=user_id, generation_task_id=generation_task_id, name=video_name, file_url=file_url, diff --git a/apps/worker/worker_app/tasks/edit_plan_generation.py b/apps/worker/worker_app/tasks/edit_plan_generation.py index cd1db6ace..799552996 100755 --- a/apps/worker/worker_app/tasks/edit_plan_generation.py +++ b/apps/worker/worker_app/tasks/edit_plan_generation.py @@ -150,6 +150,7 @@ def _finalize_render_success( create_video_record_and_dedup( generation_task_id=generation_task_id, project_id=project_id, + user_id=plan.created_by_user_id or "", batch_id=batch_id, file_url=output_url or "", file_size=file_size, diff --git a/apps/worker/worker_app/tasks/generation.py b/apps/worker/worker_app/tasks/generation.py index 1a3fc6e7e..c062a803f 100755 --- a/apps/worker/worker_app/tasks/generation.py +++ b/apps/worker/worker_app/tasks/generation.py @@ -1319,6 +1319,7 @@ def _upload_and_record( project_id: str, batch_id: str, editing_mode, + user_id: str = "", ) -> tuple[str, float, int, int]: """上传 OSS、创建视频记录并查重。 @@ -1366,6 +1367,7 @@ def _upload_and_record( video_count = create_video_record_and_dedup( generation_task_id=task_id, project_id=project_id, + user_id=user_id, batch_id=batch_id, file_url=file_url, file_size=file_size, @@ -1511,6 +1513,7 @@ def generate_video(self, task_id: str) -> dict: project_id=project_id, batch_id=batch_id, editing_mode=editing_mode, + user_id=user_id, ) if gen_task: diff --git a/packages/adapters/sqlalchemy_impl/generated_video_repository.py b/packages/adapters/sqlalchemy_impl/generated_video_repository.py index 34cba7809..44569958f 100755 --- a/packages/adapters/sqlalchemy_impl/generated_video_repository.py +++ b/packages/adapters/sqlalchemy_impl/generated_video_repository.py @@ -14,6 +14,7 @@ class SQLAlchemyGeneratedVideoRepository: model = GeneratedVideoModel( id=video.id, project_id=video.project_id, + user_id=video.user_id, generation_task_id=video.generation_task_id, name=video.name, file_url=video.file_url, @@ -43,6 +44,7 @@ class SQLAlchemyGeneratedVideoRepository: return GeneratedVideo( id=model.id, project_id=model.project_id, + user_id=getattr(model, "user_id", ""), generation_task_id=model.generation_task_id, name=model.name, file_url=model.file_url, @@ -103,15 +105,18 @@ class SQLAlchemyGeneratedVideoRepository: def list_paginated( self, *, + user_id: str | None = None, project_id: str | None = None, status: str | None = None, review_status: str | None = None, page: int = 1, page_size: int = 20, ) -> tuple[list[GeneratedVideo], int]: - """分页查询成片列表,支持按项目、状态、复核状态筛选。""" + """分页查询成片列表,支持按用户、项目、状态、复核状态筛选。""" query = self.session.query(GeneratedVideoModel) + if user_id: + query = query.filter(GeneratedVideoModel.user_id == user_id) if project_id: query = query.filter(GeneratedVideoModel.project_id == project_id) if status: @@ -183,6 +188,7 @@ class SQLAlchemyGeneratedVideoRepository: return GeneratedVideo( id=model.id, project_id=model.project_id, + user_id=getattr(model, "user_id", ""), generation_task_id=model.generation_task_id, name=model.name, file_url=model.file_url, diff --git a/packages/adapters/sqlalchemy_impl/models.py b/packages/adapters/sqlalchemy_impl/models.py index bd90e538a..dd27c570a 100755 --- a/packages/adapters/sqlalchemy_impl/models.py +++ b/packages/adapters/sqlalchemy_impl/models.py @@ -282,6 +282,7 @@ class GeneratedVideoModel(Base): id = Column(String(36), primary_key=True) project_id = Column(String(36), nullable=False, index=True) + user_id = Column(String(36), nullable=False, default="", index=True) generation_task_id = Column(String(36), nullable=False, index=True) name = Column(String(255), nullable=False) # file_url: 完整可访问的 URL,用于客户端直接访问视频 diff --git a/packages/application/generated_videos.py b/packages/application/generated_videos.py index ac2974ed3..aa8a3d4cd 100755 --- a/packages/application/generated_videos.py +++ b/packages/application/generated_videos.py @@ -21,6 +21,7 @@ class ListGeneratedVideosPaginatedUseCase: def execute( self, *, + user_id: str | None = None, project_id: str | None = None, status: str | None = None, review_status: str | None = None, @@ -32,6 +33,7 @@ class ListGeneratedVideosPaginatedUseCase: if page_size < 1 or page_size > 100: page_size = 20 return self.generated_video_repository.list_paginated( + user_id=user_id, project_id=project_id, status=status, review_status=review_status, diff --git a/packages/domain/generated_video.py b/packages/domain/generated_video.py old mode 100644 new mode 100755 index 1d95b1fb3..dcef4b437 --- a/packages/domain/generated_video.py +++ b/packages/domain/generated_video.py @@ -10,6 +10,7 @@ from uuid import uuid4 class GeneratedVideo: id: str project_id: str + user_id: str generation_task_id: str name: str file_url: str @@ -32,6 +33,7 @@ class GeneratedVideo: def create( cls, project_id: str, + user_id: str, generation_task_id: str, name: str, file_url: str, @@ -55,6 +57,7 @@ class GeneratedVideo: return cls( id=uuid4().hex, project_id=project_id.strip(), + user_id=user_id.strip(), generation_task_id=generation_task_id.strip(), name=name.strip(), file_url=file_url.strip(), diff --git a/packages/ports/generated_video_repository.py b/packages/ports/generated_video_repository.py index 1fe0b5034..d23771b20 100755 --- a/packages/ports/generated_video_repository.py +++ b/packages/ports/generated_video_repository.py @@ -19,6 +19,7 @@ class GeneratedVideoRepository(Protocol): def list_paginated( self, *, + user_id: str | None = None, project_id: str | None = None, status: str | None = None, review_status: str | None = None, diff --git a/tests/unit/test_generated_video_creation_logic.py b/tests/unit/test_generated_video_creation_logic.py old mode 100644 new mode 100755 index c0ad2a8ef..930485b16 --- a/tests/unit/test_generated_video_creation_logic.py +++ b/tests/unit/test_generated_video_creation_logic.py @@ -133,6 +133,7 @@ class TestVideoNameParameter: with pytest.raises(ValueError, match="name cannot be empty"): GeneratedVideo.create( project_id="proj-1", + user_id="user-1", generation_task_id="task-1", name="", file_url="https://example.com/test.mp4", @@ -157,3 +158,131 @@ class TestVideoNameParameter: title_text = "我的旅行vlog" video_name_3 = title_text.strip() or f"generated-{generation_task_id[:8]}.mp4" assert video_name_3 == "我的旅行vlog" + + +class TestUserIdFilter: + """验证成片库按 user_id 过滤的核心逻辑。""" + + def _setup_repo(self): + from sqlalchemy import create_engine + from sqlalchemy.orm import sessionmaker + + from packages.adapters.sqlalchemy_impl.generated_video_repository import ( + SQLAlchemyGeneratedVideoRepository, + ) + from packages.adapters.sqlalchemy_impl.models import Base + + engine = create_engine("sqlite:///:memory:") + Base.metadata.create_all(engine) + session = sessionmaker(bind=engine)() + return SQLAlchemyGeneratedVideoRepository(session), session + + def test_list_paginated_filters_by_user_id(self): + """list_paginated 传入 user_id 时只返回该用户的视频。""" + from packages.domain import GeneratedVideo + + repo, session = self._setup_repo() + try: + # 用户A的2个视频 + for i in range(2): + v = GeneratedVideo.create( + project_id=f"proj-a-{i}", + user_id="user-a", + generation_task_id=f"task-a-{i}", + name=f"video-a-{i}.mp4", + file_url=f"https://oss.example.com/a-{i}.mp4", + ) + repo.create(v) + + # 用户B的3个视频 + for i in range(3): + v = GeneratedVideo.create( + project_id=f"proj-b-{i}", + user_id="user-b", + generation_task_id=f"task-b-{i}", + name=f"video-b-{i}.mp4", + file_url=f"https://oss.example.com/b-{i}.mp4", + ) + repo.create(v) + + # 查用户A → 2条 + items, total = repo.list_paginated(user_id="user-a", page=1, page_size=10) + assert total == 2 + assert len(items) == 2 + assert all(it.user_id == "user-a" for it in items) + + # 查用户B → 3条 + items, total = repo.list_paginated(user_id="user-b", page=1, page_size=10) + assert total == 3 + assert len(items) == 3 + assert all(it.user_id == "user-b" for it in items) + finally: + session.close() + + def test_list_paginated_user_id_plus_project_id(self): + """同时传 user_id 和 project_id 时两个条件同时过滤。""" + from packages.domain import GeneratedVideo + + repo, session = self._setup_repo() + try: + # 用户A的proj-1视频 + v1 = GeneratedVideo.create( + project_id="proj-1", + user_id="user-a", + generation_task_id="task-1", + name="v1.mp4", + file_url="https://oss.example.com/v1.mp4", + ) + repo.create(v1) + + # 用户B的proj-1视频(不同用户同项目) + v2 = GeneratedVideo.create( + project_id="proj-1", + user_id="user-b", + generation_task_id="task-2", + name="v2.mp4", + file_url="https://oss.example.com/v2.mp4", + ) + repo.create(v2) + + # 用户A + proj-1 → 只有1条 + items, total = repo.list_paginated(user_id="user-a", project_id="proj-1", page=1, page_size=10) + assert total == 1 + assert items[0].user_id == "user-a" + assert items[0].generation_task_id == "task-1" + finally: + session.close() + + def test_generated_video_has_user_id_field(self): + """GeneratedVideo domain 对象有 user_id 字段。""" + from packages.domain import GeneratedVideo + + v = GeneratedVideo.create( + project_id="proj-1", + user_id="user-123", + generation_task_id="task-1", + name="test.mp4", + file_url="https://example.com/test.mp4", + ) + assert v.user_id == "user-123" + + def test_created_video_persists_user_id(self): + """创建视频后 user_id 能正确持久化和读取。""" + from packages.domain import GeneratedVideo + + repo, session = self._setup_repo() + try: + v = GeneratedVideo.create( + project_id="proj-1", + user_id="user-persist-test", + generation_task_id="task-persist", + name="persist.mp4", + file_url="https://example.com/persist.mp4", + ) + repo.create(v) + + fetched = repo.get(v.id) + assert fetched is not None + assert fetched.user_id == "user-persist-test" + finally: + session.close() -- 2.54.0 From 8bb333ccfe64588caa4d7e907ae77791ee4965f9 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Sun, 19 Jul 2026 09:28:12 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20GeneratedVideo.create()=20user=5Fid?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E5=8F=AF=E9=80=89=E5=8F=82=E6=95=B0=EF=BC=8C?= =?UTF-8?q?=E4=BF=9D=E6=8C=81=E5=90=91=E5=90=8E=E5=85=BC=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit user_id移到关键字参数位置并给默认值空串,避免破坏已有测试调用 --- packages/domain/generated_video.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/domain/generated_video.py b/packages/domain/generated_video.py index dcef4b437..9d15e8c69 100755 --- a/packages/domain/generated_video.py +++ b/packages/domain/generated_video.py @@ -10,7 +10,6 @@ from uuid import uuid4 class GeneratedVideo: id: str project_id: str - user_id: str generation_task_id: str name: str file_url: str @@ -19,6 +18,7 @@ class GeneratedVideo: width: int height: int fps: float + user_id: str = "" thumbnail_url: str | None = None status: str = "completed" review_status: str = "pending_review" @@ -33,11 +33,11 @@ class GeneratedVideo: def create( cls, project_id: str, - user_id: str, generation_task_id: str, name: str, file_url: str, *, + user_id: str = "", file_size: int = 0, duration: float = 0.0, width: int = 0, -- 2.54.0 From 36759a80c31f02c4c47507b0249254a0bd051b1a Mon Sep 17 00:00:00 2001 From: CI Bot Date: Sun, 19 Jul 2026 10:22:11 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0schema=20metadata?= =?UTF-8?q?=20snapshot=EF=BC=8C=E8=A1=A5=E5=85=85generated=5Fvideos.user?= =?UTF-8?q?=5Fid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/schema-metadata-snapshot.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/schema-metadata-snapshot.json b/docs/schema-metadata-snapshot.json index 5b1ce1dda..63cb4b869 100644 --- a/docs/schema-metadata-snapshot.json +++ b/docs/schema-metadata-snapshot.json @@ -1232,6 +1232,14 @@ "type": "VARCHAR(36)", "unique": false }, + { + "index": true, + "name": "user_id", + "nullable": false, + "primary_key": false, + "type": "VARCHAR(36)", + "unique": false + }, { "index": true, "name": "generation_task_id", @@ -1405,6 +1413,13 @@ ], "name": "ix_generated_videos_status", "unique": false + }, + { + "columns": [ + "user_id" + ], + "name": "ix_generated_videos_user_id", + "unique": false } ], "primary_key": [ -- 2.54.0