Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions backend/alembic/versions/002_simplify_roles.py
Original file line number Diff line number Diff line change
@@ -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
59 changes: 59 additions & 0 deletions backend/alembic/versions/003_add_content_communities.py
Original file line number Diff line number Diff line change
@@ -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")
81 changes: 81 additions & 0 deletions backend/alembic/versions/004_add_people_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""新增人脉模块:person_profiles + community_roles 表

Revision ID: 004_add_people_module
Revises: 003_add_content_communities
Create Date: 2026-02-22
"""

import sqlalchemy as sa
from alembic import op

revision = "004_add_people_module"
down_revision = "003_add_content_communities"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_table(
"person_profiles",
sa.Column("id", sa.Integer, primary_key=True),
sa.Column("display_name", sa.String(200), nullable=False),
sa.Column("avatar_url", sa.String(500), nullable=True),
sa.Column("github_handle", sa.String(100), nullable=True, unique=True),
sa.Column("gitcode_handle", sa.String(100), nullable=True, unique=True),
sa.Column("email", sa.String(200), nullable=True, unique=True),
sa.Column("phone", sa.String(50), nullable=True),
sa.Column("company", sa.String(200), nullable=True),
sa.Column("location", sa.String(200), nullable=True),
sa.Column("bio", sa.Text, nullable=True),
sa.Column("tags", sa.JSON, nullable=True),
sa.Column("notes", sa.Text, nullable=True),
sa.Column("source", sa.String(30), nullable=False, server_default="manual"),
sa.Column(
"created_by_id",
sa.Integer,
sa.ForeignKey("users.id", ondelete="SET NULL"),
nullable=True,
),
sa.Column("created_at", sa.DateTime, server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime, server_default=sa.func.now()),
)
op.create_index("ix_person_profiles_display_name", "person_profiles", ["display_name"])
op.create_index("ix_person_profiles_github_handle", "person_profiles", ["github_handle"])
op.create_index("ix_person_profiles_email", "person_profiles", ["email"])
op.create_index("ix_person_profiles_company", "person_profiles", ["company"])

op.create_table(
"community_roles",
sa.Column("id", sa.Integer, primary_key=True),
sa.Column(
"person_id",
sa.Integer,
sa.ForeignKey("person_profiles.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("community_name", sa.String(200), nullable=False),
sa.Column("project_url", sa.String(500), nullable=True),
sa.Column("role", sa.String(100), nullable=False),
sa.Column("role_label", sa.String(100), nullable=True),
sa.Column("is_current", sa.Boolean, server_default="1"),
sa.Column("started_at", sa.Date, nullable=True),
sa.Column("ended_at", sa.Date, nullable=True),
sa.Column("source_url", sa.String(500), nullable=True),
sa.Column(
"updated_by_id",
sa.Integer,
sa.ForeignKey("users.id", ondelete="SET NULL"),
nullable=True,
),
)
op.create_index("ix_community_roles_person_id", "community_roles", ["person_id"])


def downgrade() -> None:
op.drop_index("ix_community_roles_person_id", "community_roles")
op.drop_table("community_roles")
op.drop_index("ix_person_profiles_company", "person_profiles")
op.drop_index("ix_person_profiles_email", "person_profiles")
op.drop_index("ix_person_profiles_github_handle", "person_profiles")
op.drop_index("ix_person_profiles_display_name", "person_profiles")
op.drop_table("person_profiles")
178 changes: 178 additions & 0 deletions backend/alembic/versions/005_add_event_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"""新增活动模块:event_templates、checklist_template_items、events、
checklist_items、event_personnel、event_attendees、
feedback_items、issue_links、event_tasks 表

Revision ID: 005_add_event_module
Revises: 004_add_people_module
Create Date: 2026-02-22
"""

import sqlalchemy as sa
from alembic import op

revision = "005_add_event_module"
down_revision = "004_add_people_module"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_table(
"event_templates",
sa.Column("id", sa.Integer, primary_key=True),
sa.Column("community_id", sa.Integer, sa.ForeignKey("communities.id", ondelete="CASCADE"), nullable=False),
sa.Column("name", sa.String(200), nullable=False),
sa.Column("event_type", sa.String(20), nullable=False),
sa.Column("description", sa.Text, nullable=True),
sa.Column("is_public", sa.Boolean, server_default="0"),
sa.Column("created_by_id", sa.Integer, sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True),
sa.Column("created_at", sa.DateTime, server_default=sa.func.now()),
)
op.create_index("ix_event_templates_community_id", "event_templates", ["community_id"])

op.create_table(
"checklist_template_items",
sa.Column("id", sa.Integer, primary_key=True),
sa.Column("template_id", sa.Integer, sa.ForeignKey("event_templates.id", ondelete="CASCADE"), nullable=False),
sa.Column("phase", sa.String(10), nullable=False),
sa.Column("title", sa.String(300), nullable=False),
sa.Column("description", sa.Text, nullable=True),
sa.Column("order", sa.Integer, server_default="0"),
)
op.create_index("ix_checklist_template_items_template_id", "checklist_template_items", ["template_id"])

op.create_table(
"events",
sa.Column("id", sa.Integer, primary_key=True),
sa.Column("community_id", sa.Integer, sa.ForeignKey("communities.id", ondelete="CASCADE"), nullable=False),
sa.Column("title", sa.String(300), nullable=False),
sa.Column("event_type", sa.String(20), nullable=False, server_default="offline"),
sa.Column("template_id", sa.Integer, sa.ForeignKey("event_templates.id", ondelete="SET NULL"), nullable=True),
sa.Column("status", sa.String(20), server_default="draft"),
sa.Column("planned_at", sa.DateTime, nullable=True),
sa.Column("duration_minutes", sa.Integer, nullable=True),
sa.Column("location", sa.String(300), nullable=True),
sa.Column("online_url", sa.String(500), nullable=True),
sa.Column("description", sa.Text, nullable=True),
sa.Column("cover_image_url", sa.String(500), nullable=True),
sa.Column("owner_id", sa.Integer, sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True),
sa.Column("attendee_count", sa.Integer, nullable=True),
sa.Column("online_count", sa.Integer, nullable=True),
sa.Column("offline_count", sa.Integer, nullable=True),
sa.Column("registration_count", sa.Integer, nullable=True),
sa.Column("result_summary", sa.Text, nullable=True),
sa.Column("media_urls", sa.JSON, nullable=True),
sa.Column("created_at", sa.DateTime, server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime, server_default=sa.func.now()),
)
op.create_index("ix_events_community_id", "events", ["community_id"])

op.create_table(
"checklist_items",
sa.Column("id", sa.Integer, primary_key=True),
sa.Column("event_id", sa.Integer, sa.ForeignKey("events.id", ondelete="CASCADE"), nullable=False),
sa.Column("phase", sa.String(10), nullable=False),
sa.Column("title", sa.String(300), nullable=False),
sa.Column("status", sa.String(20), server_default="pending"),
sa.Column("assignee_id", sa.Integer, sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True),
sa.Column("due_date", sa.Date, nullable=True),
sa.Column("notes", sa.Text, nullable=True),
sa.Column("order", sa.Integer, server_default="0"),
)
op.create_index("ix_checklist_items_event_id", "checklist_items", ["event_id"])

op.create_table(
"event_personnel",
sa.Column("id", sa.Integer, primary_key=True),
sa.Column("event_id", sa.Integer, sa.ForeignKey("events.id", ondelete="CASCADE"), nullable=False),
sa.Column("role", sa.String(50), nullable=False),
sa.Column("role_label", sa.String(100), nullable=True),
sa.Column("assignee_type", sa.String(20), nullable=False),
sa.Column("user_id", sa.Integer, sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True),
sa.Column("person_id", sa.Integer, sa.ForeignKey("person_profiles.id", ondelete="SET NULL"), nullable=True),
sa.Column("confirmed", sa.String(20), server_default="pending"),
sa.Column("time_slot", sa.String(100), nullable=True),
sa.Column("notes", sa.Text, nullable=True),
sa.Column("order", sa.Integer, server_default="0"),
)
op.create_index("ix_event_personnel_event_id", "event_personnel", ["event_id"])

op.create_table(
"event_attendees",
sa.Column("id", sa.Integer, primary_key=True),
sa.Column("event_id", sa.Integer, sa.ForeignKey("events.id", ondelete="CASCADE"), nullable=False),
sa.Column("person_id", sa.Integer, sa.ForeignKey("person_profiles.id", ondelete="CASCADE"), nullable=False),
sa.Column("checked_in", sa.Boolean, server_default="0"),
sa.Column("role_at_event", sa.String(100), nullable=True),
sa.Column("source", sa.String(20), server_default="manual"),
)
op.create_index("ix_event_attendees_event_id", "event_attendees", ["event_id"])
op.create_index("ix_event_attendees_person_id", "event_attendees", ["person_id"])

op.create_table(
"feedback_items",
sa.Column("id", sa.Integer, primary_key=True),
sa.Column("event_id", sa.Integer, sa.ForeignKey("events.id", ondelete="CASCADE"), nullable=False),
sa.Column("content", sa.Text, nullable=False),
sa.Column("category", sa.String(50), server_default="question"),
sa.Column("raised_by", sa.String(200), nullable=True),
sa.Column("raised_by_person_id", sa.Integer, sa.ForeignKey("person_profiles.id", ondelete="SET NULL"), nullable=True),
sa.Column("status", sa.String(20), server_default="open"),
sa.Column("assignee_id", sa.Integer, sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True),
sa.Column("created_at", sa.DateTime, server_default=sa.func.now()),
)
op.create_index("ix_feedback_items_event_id", "feedback_items", ["event_id"])

op.create_table(
"issue_links",
sa.Column("id", sa.Integer, primary_key=True),
sa.Column("feedback_id", sa.Integer, sa.ForeignKey("feedback_items.id", ondelete="CASCADE"), nullable=False),
sa.Column("platform", sa.String(20), nullable=False),
sa.Column("repo", sa.String(200), nullable=False),
sa.Column("issue_number", sa.Integer, nullable=False),
sa.Column("issue_url", sa.String(500), nullable=False),
sa.Column("issue_type", sa.String(10), server_default="issue"),
sa.Column("issue_status", sa.String(10), server_default="open"),
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),
)
op.create_index("ix_issue_links_feedback_id", "issue_links", ["feedback_id"])

op.create_table(
"event_tasks",
sa.Column("id", sa.Integer, primary_key=True),
sa.Column("event_id", sa.Integer, sa.ForeignKey("events.id", ondelete="CASCADE"), nullable=False),
sa.Column("title", sa.String(300), nullable=False),
sa.Column("task_type", sa.String(20), server_default="task"),
sa.Column("phase", sa.String(10), server_default="pre"),
sa.Column("start_date", sa.Date, nullable=True),
sa.Column("end_date", sa.Date, nullable=True),
sa.Column("progress", sa.Integer, server_default="0"),
sa.Column("status", sa.String(20), server_default="not_started"),
sa.Column("depends_on", sa.JSON, nullable=True),
sa.Column("parent_task_id", sa.Integer, sa.ForeignKey("event_tasks.id", ondelete="SET NULL"), nullable=True),
sa.Column("order", sa.Integer, server_default="0"),
)
op.create_index("ix_event_tasks_event_id", "event_tasks", ["event_id"])


def downgrade() -> None:
op.drop_index("ix_event_tasks_event_id", "event_tasks")
op.drop_table("event_tasks")
op.drop_index("ix_issue_links_feedback_id", "issue_links")
op.drop_table("issue_links")
op.drop_index("ix_feedback_items_event_id", "feedback_items")
op.drop_table("feedback_items")
op.drop_index("ix_event_attendees_person_id", "event_attendees")
op.drop_index("ix_event_attendees_event_id", "event_attendees")
op.drop_table("event_attendees")
op.drop_index("ix_event_personnel_event_id", "event_personnel")
op.drop_table("event_personnel")
op.drop_index("ix_checklist_items_event_id", "checklist_items")
op.drop_table("checklist_items")
op.drop_index("ix_events_community_id", "events")
op.drop_table("events")
op.drop_index("ix_checklist_template_items_template_id", "checklist_template_items")
op.drop_table("checklist_template_items")
op.drop_index("ix_event_templates_community_id", "event_templates")
op.drop_table("event_templates")
35 changes: 35 additions & 0 deletions backend/alembic/versions/006_link_committee_member_to_person.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""CommitteeMember 关联 PersonProfile

Revision ID: 006_link_committee_member_to_person
Revises: 005_add_event_module
Create Date: 2026-02-22
"""

import sqlalchemy as sa
from alembic import op

revision = "006_link_committee_member_to_person"
down_revision = "005_add_event_module"
branch_labels = None
depends_on = None


def upgrade() -> None:
with op.batch_alter_table("committee_members") as batch_op:
batch_op.add_column(
sa.Column("person_id", sa.Integer, nullable=True)
)
batch_op.create_foreign_key(
"fk_committee_members_person_id",
"person_profiles",
["person_id"],
["id"],
ondelete="SET NULL",
)
batch_op.create_index("ix_committee_members_person_id", ["person_id"])


def downgrade() -> None:
with op.batch_alter_table("committee_members") as batch_op:
batch_op.drop_index("ix_committee_members_person_id")
batch_op.drop_column("person_id")
Loading
Loading