From d60fa86d70fd05368ab682696777bb838f9ca2eb Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Tue, 15 Sep 2026 08:41:15 +0800 Subject: [PATCH] feat: add Alembic migration 076 for membership & points system (#1895) - Add is_member, member_type, member_expires_at, points_balance to users table - Create points_accounts, points_transactions, points_orders, daily_usage_records tables - Include proper indexes, constraints and downgrade path --- alembic/versions/076_membership_points.py | 131 ++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 alembic/versions/076_membership_points.py diff --git a/alembic/versions/076_membership_points.py b/alembic/versions/076_membership_points.py new file mode 100644 index 000000000..3a04fd8d9 --- /dev/null +++ b/alembic/versions/076_membership_points.py @@ -0,0 +1,131 @@ +"""add membership & points system + +Revision ID: 076_membership_points +Revises: 075_add_sentence_timings +Create Date: 2026-09-15 +""" + +import sqlalchemy as sa +from sqlalchemy import text + +from alembic import op + +revision = "076_membership_points" +down_revision = "075_add_sentence_timings" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # 1. users 表新增字段 + with op.batch_alter_table("users") as batch: + batch.add_column( + sa.Column("is_member", sa.Boolean(), nullable=False, server_default=sa.text("false")), + ) + batch.add_column( + sa.Column("member_type", sa.String(20), nullable=True), + ) + batch.add_column( + sa.Column("member_expires_at", sa.DateTime(), nullable=True), + ) + batch.add_column( + sa.Column("points_balance", sa.Integer(), nullable=False, server_default=sa.text("0")), + ) + + # 2. points_accounts 积分账户表 + op.create_table( + "points_accounts", + sa.Column("id", sa.String(36), primary_key=True), + sa.Column("user_id", sa.String(36), nullable=False, unique=True, index=True), + sa.Column("balance", sa.Integer(), nullable=False, server_default=sa.text("0")), + sa.Column("total_earned", sa.Integer(), nullable=False, server_default=sa.text("0")), + sa.Column("total_spent", sa.Integer(), nullable=False, server_default=sa.text("0")), + sa.Column( + "created_at", + sa.DateTime(), + nullable=False, + server_default=sa.text("NOW()"), + ), + sa.Column( + "updated_at", + sa.DateTime(), + nullable=False, + server_default=sa.text("NOW()"), + ), + ) + + # 3. points_transactions 积分流水表 + op.create_table( + "points_transactions", + sa.Column("id", sa.String(36), primary_key=True), + sa.Column("user_id", sa.String(36), nullable=False, index=True), + sa.Column("account_id", sa.String(36), nullable=False, index=True), + sa.Column("type", sa.String(20), nullable=False, index=True), + sa.Column("source", sa.String(50), nullable=False, index=True), + sa.Column("amount", sa.Integer(), nullable=False), + sa.Column("balance_after", sa.Integer(), nullable=False), + sa.Column("description", sa.String(255), nullable=False, server_default=""), + sa.Column("ref_id", sa.String(100), nullable=False, server_default=""), + sa.Column( + "created_at", + sa.DateTime(), + nullable=False, + server_default=sa.text("NOW()"), + ), + ) + + # 4. points_orders 积分/会员订单表 + op.create_table( + "points_orders", + sa.Column("id", sa.String(36), primary_key=True), + sa.Column("user_id", sa.String(36), nullable=False, index=True), + sa.Column("order_type", sa.String(20), nullable=False), + sa.Column("product_code", sa.String(50), nullable=False), + sa.Column("amount_cents", sa.Integer(), nullable=False), + sa.Column("original_amount_cents", sa.Integer(), nullable=False, server_default=sa.text("0")), + sa.Column("discount", sa.Float(), nullable=False, server_default=sa.text("1.0")), + sa.Column("points_amount", sa.Integer(), nullable=False, server_default=sa.text("0")), + sa.Column("status", sa.String(20), nullable=False, server_default="pending", index=True), + sa.Column("payment_method", sa.String(50), nullable=True), + sa.Column("payment_id", sa.String(100), nullable=True), + sa.Column("paid_at", sa.DateTime(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(), + nullable=False, + server_default=sa.text("NOW()"), + ), + ) + + # 5. daily_usage_records 每日使用记录表 + op.create_table( + "daily_usage_records", + sa.Column("id", sa.String(36), primary_key=True), + sa.Column("user_id", sa.String(36), nullable=False, index=True), + sa.Column("usage_date", sa.DateTime(), nullable=False), + sa.Column("usage_type", sa.String(50), nullable=False, server_default="free_clip"), + sa.Column("count", sa.Integer(), nullable=False, server_default=sa.text("0")), + sa.Column( + "updated_at", + sa.DateTime(), + nullable=False, + server_default=sa.text("NOW()"), + ), + sa.UniqueConstraint( + "user_id", "usage_date", "usage_type", + name="uq_daily_usage_user_date_type", + ), + ) + + +def downgrade() -> None: + op.drop_table("daily_usage_records") + op.drop_table("points_orders") + op.drop_table("points_transactions") + op.drop_table("points_accounts") + + with op.batch_alter_table("users") as batch: + batch.drop_column("points_balance") + batch.drop_column("member_expires_at") + batch.drop_column("member_type") + batch.drop_column("is_member")