Initial project commit

This commit is contained in:
2026-05-09 16:40:29 +08:00
commit 02b0259a9e
267 changed files with 54891 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
import asyncio
from logging.config import fileConfig
from alembic import context
from sqlalchemy import pool
from sqlalchemy.ext.asyncio import async_engine_from_config
from app.core.config import settings
from app.db.base import Base
import app.models # noqa: F401 ensure all models are registered
config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
config.set_main_option("sqlalchemy.url", settings.DATABASE_URL)
target_metadata = Base.metadata
def run_migrations_offline() -> None:
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def do_run_migrations(connection):
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
async def run_async_migrations() -> None:
connectable = async_engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
def run_migrations_online() -> None:
asyncio.run(run_async_migrations())
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
+26
View File
@@ -0,0 +1,26 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}
+95
View File
@@ -0,0 +1,95 @@
"""initial schema
Revision ID: 0001
Revises:
Create Date: 2026-03-27
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from geoalchemy2 import Geometry
revision: str = "0001"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.execute("CREATE EXTENSION IF NOT EXISTS postgis")
op.create_table(
"users",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("phone", sa.String(20), unique=True, nullable=True),
sa.Column("email", sa.String(255), unique=True, nullable=True),
sa.Column("password_hash", sa.String(255), nullable=False),
sa.Column("nickname", sa.String(50), nullable=False),
sa.Column("avatar_url", sa.String(500), nullable=True),
sa.Column("city", sa.String(100), nullable=True),
sa.Column("identity", sa.String(20), server_default="both"),
sa.Column("role", sa.String(20), server_default="user"),
sa.Column("is_active", sa.Boolean, server_default=sa.text("true")),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_table(
"spots",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("title", sa.String(200), nullable=False, index=True),
sa.Column("city", sa.String(100), nullable=False, index=True),
sa.Column("location", Geometry("POINT", srid=4326), nullable=False),
sa.Column("description", sa.Text, nullable=True),
sa.Column("transport", sa.Text, nullable=True),
sa.Column("best_time", sa.String(200), nullable=True),
sa.Column("difficulty", sa.Text, nullable=True),
sa.Column("audit_status", sa.String(20), server_default="pending"),
sa.Column("reject_reason", sa.String(500), nullable=True),
sa.Column("creator_id", sa.Integer, sa.ForeignKey("users.id"), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_table(
"spot_images",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("spot_id", sa.Integer, sa.ForeignKey("spots.id"), nullable=False),
sa.Column("image_url", sa.String(500), nullable=False),
sa.Column("is_cover", sa.Boolean, server_default=sa.text("false")),
sa.Column("audit_status", sa.String(20), server_default="pending"),
sa.Column("sort_order", sa.Integer, server_default="0"),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_table(
"favorites",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("user_id", sa.Integer, sa.ForeignKey("users.id"), nullable=False),
sa.Column("spot_id", sa.Integer, sa.ForeignKey("spots.id"), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.UniqueConstraint("user_id", "spot_id", name="uq_user_spot"),
)
op.create_table(
"point_ledger",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("user_id", sa.Integer, sa.ForeignKey("users.id"), nullable=False),
sa.Column("change", sa.Integer, nullable=False),
sa.Column("balance", sa.Integer, nullable=False),
sa.Column("reason", sa.String(200), nullable=False),
sa.Column("ref_type", sa.String(50), nullable=True),
sa.Column("ref_id", sa.Integer, nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
def downgrade() -> None:
op.drop_table("point_ledger")
op.drop_table("favorites")
op.drop_table("spot_images")
op.drop_table("spots")
op.drop_table("users")
op.execute("DROP EXTENSION IF EXISTS postgis")
@@ -0,0 +1,33 @@
"""add audit_logs table
Revision ID: 0002
Revises: 0001
Create Date: 2026-03-27
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "0002"
down_revision: Union[str, None] = "0001"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"audit_logs",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("operator_id", sa.Integer, sa.ForeignKey("users.id"), nullable=False),
sa.Column("action", sa.String(100), nullable=False, index=True),
sa.Column("target_type", sa.String(50), nullable=False),
sa.Column("target_id", sa.Integer, nullable=True),
sa.Column("detail", sa.Text, nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
def downgrade() -> None:
op.drop_table("audit_logs")
@@ -0,0 +1,86 @@
"""phase2 community: comments, reports, ratings, tags, spot_tags, spot rating columns
Revision ID: 0003
Revises: 0002
Create Date: 2026-03-27
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "0003"
down_revision: Union[str, None] = "0002"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column("spots", sa.Column("avg_rating", sa.Float, nullable=True))
op.add_column("spots", sa.Column("rating_count", sa.Integer, server_default="0", nullable=False))
op.create_table(
"comments",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("spot_id", sa.Integer, sa.ForeignKey("spots.id"), nullable=False, index=True),
sa.Column("user_id", sa.Integer, sa.ForeignKey("users.id"), nullable=False),
sa.Column("parent_id", sa.Integer, sa.ForeignKey("comments.id"), nullable=True),
sa.Column("content", sa.Text, nullable=False),
sa.Column("audit_status", sa.String(20), server_default="approved"),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_table(
"reports",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("reporter_id", sa.Integer, sa.ForeignKey("users.id"), nullable=False),
sa.Column("target_type", sa.String(50), nullable=False, index=True),
sa.Column("target_id", sa.Integer, nullable=False),
sa.Column("reason", sa.Text, nullable=False),
sa.Column("status", sa.String(20), server_default="pending"),
sa.Column("handler_id", sa.Integer, sa.ForeignKey("users.id"), nullable=True),
sa.Column("conclusion", sa.Text, nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("resolved_at", sa.DateTime(timezone=True), nullable=True),
)
op.create_table(
"ratings",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("spot_id", sa.Integer, sa.ForeignKey("spots.id"), nullable=False, index=True),
sa.Column("user_id", sa.Integer, sa.ForeignKey("users.id"), nullable=False),
sa.Column("score", sa.Integer, nullable=False),
sa.Column("short_comment", sa.String(200), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.UniqueConstraint("user_id", "spot_id", name="uq_user_spot_rating"),
)
op.create_table(
"tags",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("name", sa.String(50), unique=True, nullable=False),
sa.Column("category", sa.String(50), nullable=True),
sa.Column("is_active", sa.Boolean, server_default="true"),
sa.Column("usage_count", sa.Integer, server_default="0"),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_table(
"spot_tags",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("spot_id", sa.Integer, sa.ForeignKey("spots.id"), nullable=False),
sa.Column("tag_id", sa.Integer, sa.ForeignKey("tags.id"), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.UniqueConstraint("spot_id", "tag_id", name="uq_spot_tag"),
)
def downgrade() -> None:
op.drop_table("spot_tags")
op.drop_table("tags")
op.drop_table("ratings")
op.drop_table("reports")
op.drop_table("comments")
op.drop_column("spots", "rating_count")
op.drop_column("spots", "avg_rating")
@@ -0,0 +1,23 @@
"""add is_free and price to spots
Revision ID: 0004
Revises: 0003
Create Date: 2026-03-27
"""
from alembic import op
import sqlalchemy as sa
revision = "0004"
down_revision = "0003"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column("spots", sa.Column("is_free", sa.Boolean(), server_default=sa.text("true"), nullable=False))
op.add_column("spots", sa.Column("price", sa.Numeric(10, 2), nullable=True))
def downgrade() -> None:
op.drop_column("spots", "price")
op.drop_column("spots", "is_free")
@@ -0,0 +1,23 @@
"""replace price with price_min and price_max
Revision ID: 0005
Revises: 0004
Create Date: 2026-03-27
"""
from alembic import op
import sqlalchemy as sa
revision = "0005"
down_revision = "0004"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.alter_column("spots", "price", new_column_name="price_min")
op.add_column("spots", sa.Column("price_max", sa.Numeric(10, 2), nullable=True))
def downgrade() -> None:
op.drop_column("spots", "price_max")
op.alter_column("spots", "price_min", new_column_name="price")
+21
View File
@@ -0,0 +1,21 @@
"""add bio to users
Revision ID: 0006
Revises: 0005
Create Date: 2026-03-27
"""
from alembic import op
import sqlalchemy as sa
revision = "0006"
down_revision = "0005"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column("users", sa.Column("bio", sa.Text(), nullable=True))
def downgrade() -> None:
op.drop_column("users", "bio")
@@ -0,0 +1,35 @@
"""add corrections table
Revision ID: 0007
Revises: 0006
Create Date: 2026-03-29
"""
from alembic import op
import sqlalchemy as sa
revision = "0007"
down_revision = "0006"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
"corrections",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("spot_id", sa.Integer(), sa.ForeignKey("spots.id"), nullable=False),
sa.Column("user_id", sa.Integer(), sa.ForeignKey("users.id"), nullable=False),
sa.Column("field_name", sa.String(50), nullable=False),
sa.Column("suggested_value", sa.Text(), nullable=False),
sa.Column("reason", sa.Text(), nullable=True),
sa.Column("status", sa.String(20), server_default="pending", nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_index("ix_corrections_spot_id", "corrections", ["spot_id"])
op.create_index("ix_corrections_status", "corrections", ["status"])
def downgrade() -> None:
op.drop_index("ix_corrections_status", "corrections")
op.drop_index("ix_corrections_spot_id", "corrections")
op.drop_table("corrections")
@@ -0,0 +1,43 @@
"""add_favorite_count_and_notifications
Revision ID: 3b977a379456
Revises: 0007
Create Date: 2026-03-31 12:30:46.524068
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = '3b977a379456'
down_revision: Union[str, None] = '0007'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table('notifications',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('type', sa.String(length=50), nullable=False),
sa.Column('title', sa.String(length=200), nullable=False),
sa.Column('content', sa.Text(), nullable=True),
sa.Column('ref_type', sa.String(length=50), nullable=True),
sa.Column('ref_id', sa.Integer(), nullable=True),
sa.Column('is_read', sa.Boolean(), nullable=False, server_default=sa.text('false')),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['users.id']),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_notifications_type'), 'notifications', ['type'], unique=False)
op.create_index(op.f('ix_notifications_user_id'), 'notifications', ['user_id'], unique=False)
op.add_column('spots', sa.Column('favorite_count', sa.Integer(), nullable=False, server_default=sa.text('0')))
def downgrade() -> None:
op.drop_column('spots', 'favorite_count')
op.drop_index(op.f('ix_notifications_user_id'), table_name='notifications')
op.drop_index(op.f('ix_notifications_type'), table_name='notifications')
op.drop_table('notifications')
@@ -0,0 +1,69 @@
"""add_shooting_system
Revision ID: 7bf40aa6c4b5
Revises: 3b977a379456
Create Date: 2026-03-31 12:41:00.407691
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = '7bf40aa6c4b5'
down_revision: Union[str, None] = '3b977a379456'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table('shooting_requests',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('creator_id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=200), nullable=False),
sa.Column('city', sa.String(length=100), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('style', sa.String(length=100), nullable=True),
sa.Column('shoot_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('is_free', sa.Boolean(), nullable=False),
sa.Column('budget_min', sa.Numeric(precision=10, scale=2), nullable=True),
sa.Column('budget_max', sa.Numeric(precision=10, scale=2), nullable=True),
sa.Column('role_needed', sa.String(length=20), nullable=False),
sa.Column('max_applicants', sa.Integer(), nullable=False),
sa.Column('contact_info', sa.String(length=200), nullable=True),
sa.Column('spot_id', sa.Integer(), nullable=True),
sa.Column('status', sa.String(length=20), nullable=False),
sa.Column('audit_status', sa.String(length=20), nullable=False),
sa.Column('reject_reason', sa.String(length=500), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['creator_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['spot_id'], ['spots.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_shooting_requests_city'), 'shooting_requests', ['city'], unique=False)
op.create_index(op.f('ix_shooting_requests_creator_id'), 'shooting_requests', ['creator_id'], unique=False)
op.create_index(op.f('ix_shooting_requests_status'), 'shooting_requests', ['status'], unique=False)
op.create_table('shooting_applications',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('request_id', sa.Integer(), nullable=False),
sa.Column('applicant_id', sa.Integer(), nullable=False),
sa.Column('message', sa.Text(), nullable=True),
sa.Column('status', sa.String(length=20), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['applicant_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['request_id'], ['shooting_requests.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_shooting_applications_applicant_id'), 'shooting_applications', ['applicant_id'], unique=False)
op.create_index(op.f('ix_shooting_applications_request_id'), 'shooting_applications', ['request_id'], unique=False)
def downgrade() -> None:
op.drop_index(op.f('ix_shooting_applications_request_id'), table_name='shooting_applications')
op.drop_index(op.f('ix_shooting_applications_applicant_id'), table_name='shooting_applications')
op.drop_table('shooting_applications')
op.drop_index(op.f('ix_shooting_requests_status'), table_name='shooting_requests')
op.drop_index(op.f('ix_shooting_requests_creator_id'), table_name='shooting_requests')
op.drop_index(op.f('ix_shooting_requests_city'), table_name='shooting_requests')
op.drop_table('shooting_requests')
@@ -0,0 +1,82 @@
"""add_event_system
Revision ID: a35876e08b8e
Revises: 7bf40aa6c4b5
Create Date: 2026-03-31 12:51:29.923126
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = 'a35876e08b8e'
down_revision: Union[str, None] = '7bf40aa6c4b5'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table('events',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('creator_id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=200), nullable=False),
sa.Column('city', sa.String(length=100), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('cover_url', sa.String(length=500), nullable=True),
sa.Column('location_name', sa.String(length=200), nullable=True),
sa.Column('start_time', sa.DateTime(timezone=True), nullable=True),
sa.Column('end_time', sa.DateTime(timezone=True), nullable=True),
sa.Column('max_participants', sa.Integer(), nullable=False),
sa.Column('spot_id', sa.Integer(), nullable=True),
sa.Column('status', sa.String(length=20), nullable=False),
sa.Column('audit_status', sa.String(length=20), nullable=False),
sa.Column('reject_reason', sa.String(length=500), nullable=True),
sa.Column('registration_count', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['creator_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['spot_id'], ['spots.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_events_city'), 'events', ['city'], unique=False)
op.create_index(op.f('ix_events_creator_id'), 'events', ['creator_id'], unique=False)
op.create_index(op.f('ix_events_status'), 'events', ['status'], unique=False)
op.create_table('event_photos',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('event_id', sa.Integer(), nullable=False),
sa.Column('uploader_id', sa.Integer(), nullable=False),
sa.Column('image_url', sa.String(length=500), nullable=False),
sa.Column('caption', sa.String(length=200), nullable=True),
sa.Column('spot_id', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['event_id'], ['events.id'], ),
sa.ForeignKeyConstraint(['spot_id'], ['spots.id'], ),
sa.ForeignKeyConstraint(['uploader_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_event_photos_event_id'), 'event_photos', ['event_id'], unique=False)
op.create_table('event_registrations',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('event_id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('status', sa.String(length=20), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['event_id'], ['events.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_event_registrations_event_id'), 'event_registrations', ['event_id'], unique=False)
op.create_index(op.f('ix_event_registrations_user_id'), 'event_registrations', ['user_id'], unique=False)
def downgrade() -> None:
op.drop_index(op.f('ix_event_registrations_user_id'), table_name='event_registrations')
op.drop_index(op.f('ix_event_registrations_event_id'), table_name='event_registrations')
op.drop_table('event_registrations')
op.drop_index(op.f('ix_event_photos_event_id'), table_name='event_photos')
op.drop_table('event_photos')
op.drop_index(op.f('ix_events_status'), table_name='events')
op.drop_index(op.f('ix_events_creator_id'), table_name='events')
op.drop_index(op.f('ix_events_city'), table_name='events')
op.drop_table('events')
@@ -0,0 +1,74 @@
"""add_commercial_system
Revision ID: c28508a3a8d4
Revises: a35876e08b8e
Create Date: 2026-03-31 15:01:24.482893
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = 'c28508a3a8d4'
down_revision: Union[str, None] = 'a35876e08b8e'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table('membership_plans',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('duration_days', sa.Integer(), nullable=False),
sa.Column('price', sa.Numeric(precision=10, scale=2), nullable=False),
sa.Column('benefits', sa.Text(), nullable=True),
sa.Column('extra_uploads', sa.Integer(), nullable=False),
sa.Column('extra_top_count', sa.Integer(), nullable=False),
sa.Column('is_active', sa.Boolean(), nullable=False),
sa.Column('sort_order', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_table('promotions',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('title', sa.String(length=200), nullable=False),
sa.Column('image_url', sa.String(length=500), nullable=False),
sa.Column('link_type', sa.String(length=20), nullable=False),
sa.Column('link_id', sa.Integer(), nullable=True),
sa.Column('link_url', sa.String(length=500), nullable=True),
sa.Column('position', sa.String(length=30), nullable=False),
sa.Column('sort_order', sa.Integer(), nullable=False),
sa.Column('start_time', sa.DateTime(timezone=True), nullable=True),
sa.Column('end_time', sa.DateTime(timezone=True), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=False),
sa.Column('impressions', sa.Integer(), nullable=False),
sa.Column('clicks', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_promotions_is_active'), 'promotions', ['is_active'], unique=False)
op.create_index(op.f('ix_promotions_position'), 'promotions', ['position'], unique=False)
op.create_table('user_memberships',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('plan_id', sa.Integer(), nullable=False),
sa.Column('start_date', sa.DateTime(timezone=True), nullable=False),
sa.Column('end_date', sa.DateTime(timezone=True), nullable=False),
sa.Column('is_active', sa.Boolean(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['plan_id'], ['membership_plans.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_user_memberships_user_id'), 'user_memberships', ['user_id'], unique=False)
def downgrade() -> None:
op.drop_index(op.f('ix_user_memberships_user_id'), table_name='user_memberships')
op.drop_table('user_memberships')
op.drop_index(op.f('ix_promotions_position'), table_name='promotions')
op.drop_index(op.f('ix_promotions_is_active'), table_name='promotions')
op.drop_table('promotions')
op.drop_table('membership_plans')
@@ -0,0 +1,93 @@
"""add_app_nav_config_and_promotion_refs
Revision ID: e9f8b1234cde
Revises: c28508a3a8d4
Create Date: 2026-04-12 16:20:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "e9f8b1234cde"
down_revision: Union[str, None] = "c28508a3a8d4"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column("promotions", sa.Column("spot_id", sa.Integer(), nullable=True))
op.add_column("promotions", sa.Column("event_id", sa.Integer(), nullable=True))
op.add_column("promotions", sa.Column("shooting_id", sa.Integer(), nullable=True))
op.create_foreign_key("fk_promotions_spot_id", "promotions", "spots", ["spot_id"], ["id"])
op.create_foreign_key("fk_promotions_event_id", "promotions", "events", ["event_id"], ["id"])
op.create_foreign_key("fk_promotions_shooting_id", "promotions", "shooting_requests", ["shooting_id"], ["id"])
op.execute(
"""
UPDATE promotions
SET spot_id = link_id
WHERE link_type = 'spot' AND link_id IS NOT NULL
"""
)
op.execute(
"""
UPDATE promotions
SET event_id = link_id
WHERE link_type = 'event' AND link_id IS NOT NULL
"""
)
op.execute(
"""
UPDATE promotions
SET shooting_id = link_id
WHERE link_type = 'shooting' AND link_id IS NOT NULL
"""
)
op.create_table(
"app_nav_configs",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("key", sa.String(length=50), nullable=False),
sa.Column("label", sa.String(length=50), nullable=False),
sa.Column("page_path", sa.String(length=200), nullable=False),
sa.Column("icon", sa.String(length=50), nullable=False),
sa.Column("active_icon", sa.String(length=50), nullable=False),
sa.Column("color", sa.String(length=20), nullable=False, server_default="#999999"),
sa.Column("active_color", sa.String(length=20), nullable=False, server_default="#6366f1"),
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.text("true")),
sa.Column("sort_order", sa.Integer(), nullable=False, server_default="0"),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("key"),
)
op.create_index(op.f("ix_app_nav_configs_is_active"), "app_nav_configs", ["is_active"], unique=False)
op.create_index(op.f("ix_app_nav_configs_key"), "app_nav_configs", ["key"], unique=True)
op.execute(
"""
INSERT INTO app_nav_configs (key, label, page_path, icon, active_icon, color, active_color, is_active, sort_order)
VALUES
('discover', '发现', 'pages/index/index', 'compass', 'compass-filled', '#999999', '#6366f1', true, 1),
('activity', '活动', 'pages/activity/index', 'calendar', 'calendar-filled', '#999999', '#6366f1', true, 2),
('upload', '投稿', 'pages/spot/create', 'plusempty', 'plusempty', '#999999', '#6366f1', true, 3),
('message', '消息', 'pages/mine/notifications', 'chat', 'chat-filled', '#999999', '#6366f1', true, 4),
('mine', '我的', 'pages/mine/index', 'person', 'person-filled', '#999999', '#6366f1', true, 5)
"""
)
def downgrade() -> None:
op.drop_index(op.f("ix_app_nav_configs_key"), table_name="app_nav_configs")
op.drop_index(op.f("ix_app_nav_configs_is_active"), table_name="app_nav_configs")
op.drop_table("app_nav_configs")
op.drop_constraint("fk_promotions_shooting_id", "promotions", type_="foreignkey")
op.drop_constraint("fk_promotions_event_id", "promotions", type_="foreignkey")
op.drop_constraint("fk_promotions_spot_id", "promotions", type_="foreignkey")
op.drop_column("promotions", "shooting_id")
op.drop_column("promotions", "event_id")
op.drop_column("promotions", "spot_id")
@@ -0,0 +1,89 @@
"""add_system_configs
Revision ID: f1a2b3c4d5e6
Revises: e9f8b1234cde
Create Date: 2026-04-12 20:40:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "f1a2b3c4d5e6"
down_revision: Union[str, None] = "e9f8b1234cde"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"system_configs",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("config_key", sa.String(length=100), nullable=False),
sa.Column("category", sa.String(length=50), nullable=False),
sa.Column("title", sa.String(length=200), nullable=False),
sa.Column("config_json", sa.Text(), nullable=False, server_default="{}"),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.text("true")),
sa.Column("sort_order", sa.Integer(), nullable=False, server_default="0"),
sa.Column("updated_by", sa.Integer(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("config_key"),
)
op.create_index(op.f("ix_system_configs_config_key"), "system_configs", ["config_key"], unique=True)
op.create_index(op.f("ix_system_configs_category"), "system_configs", ["category"], unique=False)
op.create_index(op.f("ix_system_configs_is_active"), "system_configs", ["is_active"], unique=False)
system_configs_table = sa.table(
"system_configs",
sa.column("config_key", sa.String(length=100)),
sa.column("category", sa.String(length=50)),
sa.column("title", sa.String(length=200)),
sa.column("config_json", sa.Text()),
sa.column("description", sa.Text()),
sa.column("is_active", sa.Boolean()),
sa.column("sort_order", sa.Integer()),
)
op.bulk_insert(
system_configs_table,
[
{
"config_key": "notification_template_default",
"category": "notification_template",
"title": "默认通知模板",
"config_json": '{"type":"system","title_template":"{title}","content_template":"{content}","channels":["in_app"]}',
"description": "系统默认通知模板(JSON字符串)",
"is_active": True,
"sort_order": 1,
},
{
"config_key": "notification_rule_auto_dispatch",
"category": "notification_rule",
"title": "通知自动触发规则",
"config_json": '{"enabled":true,"triggers":[{"event":"report_resolved","template":"notification_template_default"}],"rate_limit_per_minute":60}',
"description": "通知规则引擎配置(JSON字符串)",
"is_active": True,
"sort_order": 2,
},
{
"config_key": "report_sop_default",
"category": "report_sop",
"title": "举报处理SOP",
"config_json": '{"steps":[{"status":"pending","action":"初筛"},{"status":"processing","action":"调查取证"},{"status":"resolved","action":"处置并回执"},{"status":"rejected","action":"驳回并说明"}],"sla_hours":24}',
"description": "举报工单标准化流程(JSON字符串)",
"is_active": True,
"sort_order": 3,
},
],
)
def downgrade() -> None:
op.drop_index(op.f("ix_system_configs_is_active"), table_name="system_configs")
op.drop_index(op.f("ix_system_configs_category"), table_name="system_configs")
op.drop_index(op.f("ix_system_configs_config_key"), table_name="system_configs")
op.drop_table("system_configs")