From 30bdc45aa7574b5c5132674142b1d872877c898b Mon Sep 17 00:00:00 2001 From: Zhenyu Zheng Date: Sun, 22 Feb 2026 10:12:10 +0800 Subject: [PATCH 1/8] =?UTF-8?q?refactor:=20=E5=89=8D=E7=BD=AE=E9=87=8D?= =?UTF-8?q?=E6=9E=84=202.1=20=E2=80=94=20=E6=9D=83=E9=99=90=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E7=AE=80=E5=8C=96=EF=BC=88=E7=A7=BB=E9=99=A4=20commun?= =?UTF-8?q?ity=5Fadmin=20=E8=A7=92=E8=89=B2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 Alembic 迁移 002_simplify_roles:将 community_users.role = community_admin 统一降级为 user - dependencies.py 新增 get_current_admin_or_superuser(检查是否为 superuser 或任意社区 admin) - 保留 get_community_admin 供过渡期兼容 - committees.py / meetings.py:全员可操作,改为 get_current_user(普通用户也可编辑治理内容) - communities.py / analytics.py:平台级配置改为 get_current_admin_or_superuser(admin+ 限制) - 全部 371 个测试通过,无 regression --- .../alembic/versions/002_simplify_roles.py | 27 ++++++++++++ backend/app/api/analytics.py | 4 +- backend/app/api/committees.py | 18 ++++---- backend/app/api/communities.py | 4 +- backend/app/api/meetings.py | 18 ++++---- backend/app/core/dependencies.py | 42 +++++++++++++++++++ 6 files changed, 91 insertions(+), 22 deletions(-) create mode 100644 backend/alembic/versions/002_simplify_roles.py diff --git a/backend/alembic/versions/002_simplify_roles.py b/backend/alembic/versions/002_simplify_roles.py new file mode 100644 index 0000000..dd7e87b --- /dev/null +++ b/backend/alembic/versions/002_simplify_roles.py @@ -0,0 +1,27 @@ +"""简化权限模型:移除 community_admin 角色 + +将所有 community_users.role = 'community_admin' 统一降级为 'user'。 +新权限体系:superuser / admin / user(三层,无 community_admin) + +Revision ID: 002_simplify_roles +Revises: 001_initial +Create Date: 2026-02-22 +""" + +from alembic import op + +revision = "002_simplify_roles" +down_revision = "001_initial" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + "UPDATE community_users SET role = 'user' WHERE role = 'community_admin'" + ) + + +def downgrade() -> None: + # 无法恢复:降级后无法区分哪些 user 原本是 community_admin + pass diff --git a/backend/app/api/analytics.py b/backend/app/api/analytics.py index 5b6f42e..5f7f5f3 100644 --- a/backend/app/api/analytics.py +++ b/backend/app/api/analytics.py @@ -2,7 +2,7 @@ from sqlalchemy import func from sqlalchemy.orm import Session -from app.core.dependencies import get_community_admin, get_current_community, get_current_user +from app.core.dependencies import get_current_admin_or_superuser, get_current_community, get_current_user from app.core.logging import get_logger from app.database import get_db from app.models import User @@ -119,7 +119,7 @@ def update_channel_settings( channel: str, data: ChannelConfigUpdate, community_id: int = Depends(get_current_community), - current_user: User = Depends(get_community_admin), + current_user: User = Depends(get_current_admin_or_superuser), db: Session = Depends(get_db), ): """ diff --git a/backend/app/api/committees.py b/backend/app/api/committees.py index d227615..4fadbe3 100644 --- a/backend/app/api/committees.py +++ b/backend/app/api/committees.py @@ -7,8 +7,8 @@ from sqlalchemy.orm import Session from app.core.dependencies import ( - get_community_admin, get_current_community, + get_current_user, ) from app.database import get_db from app.models import Committee, CommitteeMember, User @@ -76,7 +76,7 @@ def list_committees( def create_committee( data: CommitteeCreate, community_id: int = Depends(get_current_community), - current_user: User = Depends(get_community_admin), + current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """创建委员会(需要社区管理员权限)。""" @@ -123,7 +123,7 @@ def update_committee( committee_id: int, data: CommitteeUpdate, community_id: int = Depends(get_current_community), - current_user: User = Depends(get_community_admin), + current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """更新委员会信息(需要社区管理员权限)。""" @@ -142,7 +142,7 @@ def update_committee( def delete_committee( committee_id: int, community_id: int = Depends(get_current_community), - current_user: User = Depends(get_community_admin), + current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """删除委员会(需要社区管理员权限,级联删除成员和会议)。""" @@ -187,7 +187,7 @@ def add_member( committee_id: int, data: CommitteeMemberCreate, community_id: int = Depends(get_current_community), - current_user: User = Depends(get_community_admin), + current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """添加委员会成员(需要社区管理员权限)。""" @@ -209,7 +209,7 @@ def add_member( def export_members_csv( committee_id: int, community_id: int = Depends(get_current_community), - current_user: User = Depends(get_community_admin), + current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """导出委员会成员为CSV文件(需要社区管理员权限)。""" @@ -262,7 +262,7 @@ def import_members_csv( committee_id: int, file: UploadFile = File(...), community_id: int = Depends(get_current_community), - current_user: User = Depends(get_community_admin), + current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """从CSV文件批量导入委员会成员(需要社区管理员权限)。 @@ -438,7 +438,7 @@ def update_member( member_id: int, data: CommitteeMemberUpdate, community_id: int = Depends(get_current_community), - current_user: User = Depends(get_community_admin), + current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """更新成员信息(需要社区管理员权限)。""" @@ -475,7 +475,7 @@ def remove_member( committee_id: int, member_id: int, community_id: int = Depends(get_current_community), - current_user: User = Depends(get_community_admin), + current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """移除委员会成员(需要社区管理员权限)。""" diff --git a/backend/app/api/communities.py b/backend/app/api/communities.py index 660a0f8..b00c1b0 100644 --- a/backend/app/api/communities.py +++ b/backend/app/api/communities.py @@ -4,8 +4,8 @@ from sqlalchemy.orm import Session, attributes from app.core.dependencies import ( - get_community_admin, get_current_active_superuser, + get_current_admin_or_superuser, get_current_user, get_user_community_role, ) @@ -203,7 +203,7 @@ class CommunityBasicUpdate(BaseModel): def update_community_basic( community_id: int, data: CommunityBasicUpdate, - current_user: User = Depends(get_community_admin), + current_user: User = Depends(get_current_admin_or_superuser), db: Session = Depends(get_db), ): """ diff --git a/backend/app/api/meetings.py b/backend/app/api/meetings.py index 4920525..30f3c75 100644 --- a/backend/app/api/meetings.py +++ b/backend/app/api/meetings.py @@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.orm import Session, joinedload -from app.core.dependencies import get_community_admin, get_current_community +from app.core.dependencies import get_current_community, get_current_user from app.database import get_db from app.models import User from app.models.committee import Committee, CommitteeMember @@ -72,7 +72,7 @@ def list_meetings( def create_meeting( data: MeetingCreate, community_id: int = Depends(get_current_community), - current_user: User = Depends(get_community_admin), + current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """ @@ -184,7 +184,7 @@ def update_meeting( meeting_id: int, data: MeetingUpdate, community_id: int = Depends(get_current_community), - current_user: User = Depends(get_community_admin), + current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """ @@ -246,7 +246,7 @@ def update_meeting( def delete_meeting( meeting_id: int, community_id: int = Depends(get_current_community), - current_user: User = Depends(get_community_admin), + current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """ @@ -276,7 +276,7 @@ def create_reminder( meeting_id: int, reminder_data: MeetingReminderCreate, community_id: int = Depends(get_current_community), - current_user: User = Depends(get_community_admin), + current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """ @@ -411,7 +411,7 @@ def add_participant( meeting_id: int, data: MeetingParticipantCreate, community_id: int = Depends(get_current_community), - current_user: User = Depends(get_community_admin), + current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """手动添加会议与会人。""" @@ -456,7 +456,7 @@ def delete_participant( meeting_id: int, participant_id: int, community_id: int = Depends(get_current_community), - current_user: User = Depends(get_community_admin), + current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """删除会议与会人。""" @@ -487,7 +487,7 @@ def delete_participant( def import_participants( meeting_id: int, community_id: int = Depends(get_current_community), - current_user: User = Depends(get_community_admin), + current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """从委员会成员导入会议与会人。""" @@ -583,7 +583,7 @@ def update_meeting_minutes( meeting_id: int, minutes_data: dict, community_id: int = Depends(get_current_community), - current_user: User = Depends(get_community_admin), + current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """ diff --git a/backend/app/core/dependencies.py b/backend/app/core/dependencies.py index 0bf06ec..3382212 100644 --- a/backend/app/core/dependencies.py +++ b/backend/app/core/dependencies.py @@ -162,6 +162,44 @@ def get_user_community_role( return result +async def get_current_admin_or_superuser( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +) -> User: + """ + Dependency to verify user is a platform admin or superuser. + + Checks if the user is a superuser OR has 'admin' role in any community. + Used for platform-level management operations (community settings, analytics config, etc.) + + Args: + current_user: Current authenticated user + db: Database session + + Returns: + User: The admin/superuser user + + Raises: + HTTPException: If user does not have admin or superuser permissions + """ + if current_user.is_superuser: + return current_user + + stmt = select(community_users.c.role).where( + community_users.c.user_id == current_user.id, + community_users.c.role == "admin", + ).limit(1) + result = db.execute(stmt).scalar() + + if not result: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="管理员权限不足", + ) + + return current_user + + async def get_community_admin( x_community_id: int | None = Header(None), user: User = Depends(get_current_user), @@ -170,6 +208,10 @@ async def get_community_admin( """ Dependency to verify user is a community admin or superuser. + Deprecated: Use get_current_admin_or_superuser for platform-level checks, + or get_current_user for business operations (committees, meetings, etc.) + kept for backward compatibility during the transition period. + Args: x_community_id: Community ID from header user: Current authenticated user From a2d0a51c25482df668267c5f1c76f6f49906f140 Mon Sep 17 00:00:00 2001 From: Zhenyu Zheng Date: Sun, 22 Feb 2026 10:26:06 +0800 Subject: [PATCH 2/8] =?UTF-8?q?refactor:=20=E5=89=8D=E7=BD=AE=E9=87=8D?= =?UTF-8?q?=E6=9E=84=202.2=20=E2=80=94=20=E5=86=85=E5=AE=B9=E5=A4=9A?= =?UTF-8?q?=E7=A4=BE=E5=8C=BA=E5=85=B3=E8=81=94=EF=BC=88content=5Fcommunit?= =?UTF-8?q?ies=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 content_communities 关联表及 Alembic 迁移(003) - Content/Community 模型新增双向 relationship(communities/linked_contents) - ContentCreate/Update/Out schema 新增 community_ids 字段 - contents API 全面切换为 content_communities 过滤逻辑: - _build_community_filter 使用 OR 兼容策略(join表 + legacy community_id 列) - _get_content_community_ids / _write_content_communities 帮助函数 - 所有路由返回 community_ids 字段 - 保留 contents.community_id 列供过渡期使用(migration 009 后移除) --- .../versions/003_add_content_communities.py | 59 ++++++++ backend/app/api/contents.py | 131 +++++++++++++++--- backend/app/models/community.py | 5 + backend/app/models/content.py | 19 ++- backend/app/schemas/content.py | 6 + 5 files changed, 199 insertions(+), 21 deletions(-) create mode 100644 backend/alembic/versions/003_add_content_communities.py diff --git a/backend/alembic/versions/003_add_content_communities.py b/backend/alembic/versions/003_add_content_communities.py new file mode 100644 index 0000000..144497f --- /dev/null +++ b/backend/alembic/versions/003_add_content_communities.py @@ -0,0 +1,59 @@ +"""新增 content_communities 关联表,迁移现有 community_id 数据 + +第一步(本文件):创建关联表,将 contents.community_id 迁移进新表(is_primary=True) +第二步(009_remove_content_community_id.py):迁移验证无误后移除旧 community_id 列 + +Revision ID: 003_add_content_communities +Revises: 002_simplify_roles +Create Date: 2026-02-22 +""" + +import sqlalchemy as sa +from alembic import op + +revision = "003_add_content_communities" +down_revision = "002_simplify_roles" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # 唯一约束直接内联到 create_table,兼容 SQLite(不支持 ALTER ADD CONSTRAINT) + op.create_table( + "content_communities", + sa.Column("id", sa.Integer, primary_key=True), + sa.Column( + "content_id", + sa.Integer, + sa.ForeignKey("contents.id", ondelete="CASCADE"), + nullable=False, + index=True, + ), + sa.Column( + "community_id", + sa.Integer, + sa.ForeignKey("communities.id", ondelete="CASCADE"), + nullable=False, + index=True, + ), + sa.Column("is_primary", sa.Boolean, server_default="1"), + sa.Column("linked_at", sa.DateTime, server_default=sa.func.now()), + sa.Column( + "linked_by_id", + sa.Integer, + sa.ForeignKey("users.id", ondelete="SET NULL"), + nullable=True, + ), + sa.UniqueConstraint("content_id", "community_id", name="uq_content_community"), + ) + # 迁移现有数据:将 contents.community_id 作为主社区写入新关联表 + op.execute(""" + INSERT INTO content_communities (content_id, community_id, is_primary) + SELECT id, community_id, 1 + FROM contents + WHERE community_id IS NOT NULL + """) + + +def downgrade() -> None: + op.drop_table("content_communities") diff --git a/backend/app/api/contents.py b/backend/app/api/contents.py index 4099fbb..1a942f7 100644 --- a/backend/app/api/contents.py +++ b/backend/app/api/contents.py @@ -1,11 +1,14 @@ from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy import insert, or_ +from sqlalchemy import select as sa_select from sqlalchemy.orm import Session from app.core.dependencies import check_content_edit_permission, get_current_community, get_current_user from app.database import get_db from app.models import Content, User +from app.models.content import content_communities from app.schemas.content import ( ContentCalendarOut, ContentCreate, @@ -23,6 +26,46 @@ VALID_SOURCE_TYPES = {"contribution", "release_note", "event_summary"} +def _build_community_filter(community_id: int): + """返回社区内容过滤条件(OR 逻辑,兼容遷移期间)。 + + 同时匹配: + 1. 已写入 content_communities 的多社区关联记录 + 2. 仍使用旧 community_id 列的内容(过渡兼容) + """ + linked_subq = sa_select(content_communities.c.content_id).where( + content_communities.c.community_id == community_id + ) + return or_( + Content.id.in_(linked_subq), + Content.community_id == community_id, + ) + + +def _get_content_community_ids(db: Session, content_id: int) -> list[int]: + """获取内容关联的所有社区 ID 列表。""" + rows = db.query(content_communities.c.community_id).filter( + content_communities.c.content_id == content_id + ).all() + return [r[0] for r in rows] + + +def _write_content_communities(db: Session, content_id: int, community_ids: list[int], linked_by_id: int | None) -> None: + """向 content_communities 写入关联行(幂等,已存在则跳过)。""" + existing = {r[0] for r in db.query(content_communities.c.community_id).filter( + content_communities.c.content_id == content_id + ).all()} + is_first = len(existing) == 0 + for i, cid in enumerate(community_ids): + if cid not in existing: + db.execute(insert(content_communities).values( + content_id=content_id, + community_id=cid, + is_primary=(is_first and i == 0), + linked_by_id=linked_by_id, + )) + + @router.get("", response_model=PaginatedContents) def list_contents( page: int = Query(1, ge=1), @@ -34,8 +77,8 @@ def list_contents( current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): - # Filter by community - query = db.query(Content).filter(Content.community_id == community_id) + # 通过 content_communities 关联表过滤(兼容遷移期间的 community_id 列) + query = db.query(Content).filter(_build_community_filter(community_id)) if status: query = query.filter(Content.status == status) @@ -77,6 +120,10 @@ def create_content( db.add(content) db.flush() # Get content ID + # 写入多社区关联(未指定则关联当前社区) + target_community_ids = data.community_ids if data.community_ids else [community_id] + _write_content_communities(db, content.id, target_community_ids, current_user.id) + # Assign assignees (default to creator if empty) — batch query to avoid N+1 assignee_ids = data.assignee_ids if data.assignee_ids else [current_user.id] assignee_users = db.query(User).filter(User.id.in_(assignee_ids)).all() @@ -85,7 +132,12 @@ def create_content( db.commit() db.refresh(content) - return content + content_dict = { + **{c.name: getattr(content, c.name) for c in content.__table__.columns}, + "assignee_ids": [a.id for a in content.assignees], + "community_ids": _get_content_community_ids(db, content.id), + } + return content_dict @router.get("/{content_id}", response_model=ContentOut) @@ -97,15 +149,15 @@ def get_content( ): content = db.query(Content).filter( Content.id == content_id, - Content.community_id == community_id, + _build_community_filter(community_id), ).first() if not content: raise HTTPException(404, "Content not found") - # Build response dict with assignee_ids content_dict = { **{c.name: getattr(content, c.name) for c in content.__table__.columns}, - "assignee_ids": [a.id for a in content.assignees] + "assignee_ids": [a.id for a in content.assignees], + "community_ids": _get_content_community_ids(db, content_id), } return content_dict @@ -120,7 +172,7 @@ def update_content( ): content = db.query(Content).filter( Content.id == content_id, - Content.community_id == community_id, + _build_community_filter(community_id), ).first() if not content: raise HTTPException(404, "Content not found") @@ -131,11 +183,30 @@ def update_content( update_data = data.model_dump(exclude_unset=True) + # Handle community_ids update (replace all associations for this content) + if "community_ids" in update_data: + new_community_ids = update_data.pop("community_ids") + if new_community_ids is not None: + db.execute( + content_communities.delete().where( + content_communities.c.content_id == content_id + ) + ) + for i, cid in enumerate(new_community_ids): + db.execute(insert(content_communities).values( + content_id=content_id, + community_id=cid, + is_primary=(i == 0), + linked_by_id=current_user.id, + )) + # 同步更新 community_id 为主社区(过渡兼容) + if new_community_ids: + content.community_id = new_community_ids[0] + # Handle assignees update if "assignee_ids" in update_data: assignee_ids = update_data.pop("assignee_ids") content.assignees.clear() - # Batch query to avoid N+1 when updating assignees if assignee_ids: assignee_users = db.query(User).filter(User.id.in_(assignee_ids)).all() for user in assignee_users: @@ -147,7 +218,12 @@ def update_content( setattr(content, key, value) db.commit() db.refresh(content) - return content + content_dict = { + **{c.name: getattr(content, c.name) for c in content.__table__.columns}, + "assignee_ids": [a.id for a in content.assignees], + "community_ids": _get_content_community_ids(db, content_id), + } + return content_dict @router.delete("/{content_id}", status_code=204) @@ -159,7 +235,7 @@ def delete_content( ): content = db.query(Content).filter( Content.id == content_id, - Content.community_id == community_id, + _build_community_filter(community_id), ).first() if not content: raise HTTPException(404, "Content not found") @@ -184,7 +260,7 @@ def update_content_status( raise HTTPException(400, f"Invalid status, must be one of {VALID_STATUSES}") content = db.query(Content).filter( Content.id == content_id, - Content.community_id == community_id, + _build_community_filter(community_id), ).first() if not content: raise HTTPException(404, "Content not found") @@ -196,7 +272,12 @@ def update_content_status( content.status = data.status db.commit() db.refresh(content) - return content + content_dict = { + **{c.name: getattr(content, c.name) for c in content.__table__.columns}, + "assignee_ids": [a.id for a in content.assignees], + "community_ids": _get_content_community_ids(db, content_id), + } + return content_dict # Collaborators Management Endpoints @@ -213,7 +294,7 @@ def list_collaborators( """ content = db.query(Content).filter( Content.id == content_id, - Content.community_id == community_id, + _build_community_filter(community_id), ).first() if not content: raise HTTPException(404, "Content not found") @@ -242,7 +323,7 @@ def add_collaborator( """ content = db.query(Content).filter( Content.id == content_id, - Content.community_id == community_id, + _build_community_filter(community_id), ).first() if not content: raise HTTPException(404, "Content not found") @@ -283,7 +364,7 @@ def remove_collaborator( """ content = db.query(Content).filter( Content.id == content_id, - Content.community_id == community_id, + _build_community_filter(community_id), ).first() if not content: raise HTTPException(404, "Content not found") @@ -317,7 +398,7 @@ def transfer_ownership( """ content = db.query(Content).filter( Content.id == content_id, - Content.community_id == community_id, + _build_community_filter(community_id), ).first() if not content: raise HTTPException(404, "Content not found") @@ -338,7 +419,12 @@ def transfer_ownership( db.commit() db.refresh(content) - return content + content_dict = { + **{c.name: getattr(content, c.name) for c in content.__table__.columns}, + "assignee_ids": [a.id for a in content.assignees], + "community_ids": _get_content_community_ids(db, content_id), + } + return content_dict # ==================== Calendar API Endpoints ==================== @@ -364,7 +450,7 @@ def list_calendar_events( except ValueError: raise HTTPException(400, "Invalid date format. Use ISO format (e.g. 2026-02-01)") from None - query = db.query(Content).filter(Content.community_id == community_id) + query = db.query(Content).filter(_build_community_filter(community_id)) if status: query = query.filter(Content.status == status) @@ -405,7 +491,7 @@ def update_content_schedule( """ content = db.query(Content).filter( Content.id == content_id, - Content.community_id == community_id, + _build_community_filter(community_id), ).first() if not content: raise HTTPException(404, "Content not found") @@ -417,4 +503,9 @@ def update_content_schedule( content.scheduled_publish_at = data.scheduled_publish_at db.commit() db.refresh(content) - return content + content_dict = { + **{c.name: getattr(content, c.name) for c in content.__table__.columns}, + "assignee_ids": [a.id for a in content.assignees], + "community_ids": _get_content_community_ids(db, content_id), + } + return content_dict diff --git a/backend/app/models/community.py b/backend/app/models/community.py index 670fb82..bbecad2 100644 --- a/backend/app/models/community.py +++ b/backend/app/models/community.py @@ -28,6 +28,11 @@ class Community(Base): back_populates="communities", ) contents = relationship("Content", back_populates="community", cascade="all, delete-orphan") + linked_contents = relationship( + "Content", + secondary="content_communities", + back_populates="communities", + ) channel_configs = relationship("ChannelConfig", back_populates="community", cascade="all, delete-orphan") audit_logs = relationship("AuditLog", back_populates="community", cascade="all, delete-orphan") committees = relationship("Committee", back_populates="community", cascade="all, delete-orphan") diff --git a/backend/app/models/content.py b/backend/app/models/content.py index 682c279..b9eb5d9 100644 --- a/backend/app/models/content.py +++ b/backend/app/models/content.py @@ -1,11 +1,23 @@ from datetime import datetime -from sqlalchemy import JSON, Column, DateTime, ForeignKey, Integer, String, Table, Text +from sqlalchemy import JSON, Boolean, Column, DateTime, ForeignKey, Integer, String, Table, Text from sqlalchemy import Enum as SAEnum from sqlalchemy.orm import relationship from app.database import Base +# Association table for content → community (multi-community support) +content_communities = Table( + "content_communities", + Base.metadata, + Column("id", Integer, primary_key=True), + Column("content_id", Integer, ForeignKey("contents.id", ondelete="CASCADE"), nullable=False, index=True), + Column("community_id", Integer, ForeignKey("communities.id", ondelete="CASCADE"), nullable=False, index=True), + Column("is_primary", Boolean, server_default="1"), + Column("linked_at", DateTime, default=datetime.utcnow), + Column("linked_by_id", Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True), +) + # Association table for content collaborators content_collaborators = Table( "content_collaborators", @@ -64,6 +76,11 @@ class Content(Base): publish_records = relationship("PublishRecord", back_populates="content", cascade="all, delete-orphan") community = relationship("Community", back_populates="contents") + communities = relationship( + "Community", + secondary="content_communities", + back_populates="linked_contents", + ) creator = relationship("User", foreign_keys=[created_by_user_id], back_populates="created_contents") owner = relationship("User", foreign_keys=[owner_id], back_populates="owned_contents") collaborators = relationship( diff --git a/backend/app/schemas/content.py b/backend/app/schemas/content.py index 81263ad..51812d1 100644 --- a/backend/app/schemas/content.py +++ b/backend/app/schemas/content.py @@ -15,6 +15,8 @@ class ContentCreate(BaseModel): scheduled_publish_at: datetime | None = None work_status: str = "planning" assignee_ids: list[int] = [] + # 多社区关联:不填则默认使用 X-Community-Id header 对应的社区 + community_ids: list[int] = [] class ContentUpdate(BaseModel): @@ -29,6 +31,8 @@ class ContentUpdate(BaseModel): scheduled_publish_at: datetime | None = None work_status: str | None = None assignee_ids: list[int] | None = None + # 多社区关联:提供则替换全部关联;不提供则不变 + community_ids: list[int] | None = None class ContentStatusUpdate(BaseModel): @@ -55,6 +59,8 @@ class ContentOut(BaseModel): created_at: datetime updated_at: datetime assignee_ids: list[int] = [] + # 多社区关联列表(由 API 层手动填充) + community_ids: list[int] = [] model_config = {"from_attributes": True} From e3f1f8dfbcc7df97b0b94c1f30af8ddeed41c82d Mon Sep 17 00:00:00 2001 From: Zhenyu Zheng Date: Sun, 22 Feb 2026 10:33:33 +0800 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=E5=89=8D=E7=BD=AE=E9=87=8D?= =?UTF-8?q?=E6=9E=84=202.3=20=E2=80=94=20=E5=89=8D=E7=AB=AF=E5=AF=BC?= =?UTF-8?q?=E8=88=AA=E9=87=8D=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 侧边栏按新信息架构重组: 社区工作台 / 个人工作看板 / 社区治理 / 内容管理 / 活动管理 / 洞察与人脉 / 平台管理 - 发布管理并入内容管理子菜单 - 平台管理(社区设置 + 超管专属)整合为统一分组 - 新增占位路由与视图:/events、/people、/campaigns (Phase 4a/4c 功能完成后逐步填充) - 我的工作改名为"个人工作看板" --- frontend/src/App.vue | 126 +++++++++++++++++++------------ frontend/src/router/index.ts | 20 +++++ frontend/src/views/Campaigns.vue | 53 +++++++++++++ frontend/src/views/Events.vue | 53 +++++++++++++ frontend/src/views/People.vue | 53 +++++++++++++ 5 files changed, 255 insertions(+), 50 deletions(-) create mode 100644 frontend/src/views/Campaigns.vue create mode 100644 frontend/src/views/Events.vue create mode 100644 frontend/src/views/People.vue diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 9b486b1..d325df0 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -20,15 +20,40 @@ text-color="#64748b" active-text-color="#0095ff" > - + 社区工作台 + + - 我的工作 + 个人工作看板 + + + + + + + + + 治理概览 + + + + 委员会 + + + + 会议管理 + + +