87f92987b2
前端: - 集成 in_app_purchase 插件,实现 IAP 支付流程 - 添加支付模块 (payments/) 处理产品获取、购买、验证 - 积分中心页面集成 Apple Pay 购买入口 - 设置页面重构: 关于/隐私/协议直接展示,删除 legal_center 子页面 - 修复欢迎引导页滚动检测阈值问题 - 修复解卦结果页 iOS 侧滑返回手势被阻止的问题 - 邀请码绑定按钮临时禁用(待后端实现) 后端: - 新增 apple_iap_transactions 表记录交易 - 实现 Apple 服务器端验证 (App Store Server API) - 支付成功后自动发放积分 - 支持 Sandbox/Production 环境切换 - 添加退款处理和交易状态机 协议: - 更新积分流水协议,支持 purchase/refund 类型 - 新增 PAYMENT_* 错误码
279 lines
12 KiB
Python
279 lines
12 KiB
Python
"""add apple_iap_transactions table and update points_ledger constraints
|
|
|
|
Revision ID: 20260427_0001
|
|
Revises: 20260417_0001
|
|
Create Date: 2026-04-27 12:00:00
|
|
|
|
Changes:
|
|
1. Create apple_iap_transactions table for Apple IAP payment tracking
|
|
2. Update points_ledger check constraints:
|
|
- Remove 'grant' from change_type (merged into 'adjust')
|
|
- Add 'purchase' and 'refund' to change_type
|
|
- Add 'payment' to biz_type
|
|
- Update biz_binding constraint for new types
|
|
- Update direction_by_change_type constraint
|
|
- Add metadata shape constraints for purchase/refund
|
|
- Update adjust metadata constraint (ticket_id -> reason)
|
|
3. Update points_audit_ledger check constraints similarly
|
|
"""
|
|
|
|
from typing import Sequence, Union
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
from sqlalchemy.dialects import postgresql
|
|
|
|
revision: str = "20260427_0001"
|
|
down_revision: Union[str, Sequence[str], None] = "20260417_0001"
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_table(
|
|
"apple_iap_transactions",
|
|
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
|
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
sa.Column("product_code", sa.String(32), nullable=False),
|
|
sa.Column("app_store_product_id", sa.String(128), nullable=False),
|
|
sa.Column("transaction_id", sa.String(64), nullable=False),
|
|
sa.Column("original_transaction_id", sa.String(64), nullable=True),
|
|
sa.Column("web_order_line_item_id", sa.String(64), nullable=True),
|
|
sa.Column("environment", sa.String(16), nullable=False),
|
|
sa.Column("bundle_id", sa.String(128), nullable=False),
|
|
sa.Column("app_account_token", postgresql.UUID(as_uuid=True), nullable=True),
|
|
sa.Column("purchase_date", sa.Text, nullable=False),
|
|
sa.Column("revocation_date", sa.Text, nullable=True),
|
|
sa.Column("status", sa.String(24), nullable=False),
|
|
sa.Column("credits", sa.BigInteger, nullable=False),
|
|
sa.Column("currency", sa.String(8), nullable=True),
|
|
sa.Column("price_milliunits", sa.BigInteger, nullable=True),
|
|
sa.Column("ledger_event_id", sa.String(64), nullable=True),
|
|
sa.Column("signed_transaction_info", sa.Text, nullable=False),
|
|
sa.Column(
|
|
"apple_payload",
|
|
postgresql.JSONB(),
|
|
nullable=False,
|
|
server_default=sa.text("'{}'::jsonb"),
|
|
),
|
|
sa.Column("failure_code", sa.String(64), 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.CheckConstraint(
|
|
"environment in ('Sandbox', 'Production')",
|
|
name="ck_apple_iap_transactions_environment",
|
|
),
|
|
sa.CheckConstraint(
|
|
"status in ('received', 'verified', 'granted', 'failed', 'refunded', 'refunded_insufficient', 'revoked')",
|
|
name="ck_apple_iap_transactions_status",
|
|
),
|
|
sa.UniqueConstraint("transaction_id", name="uq_apple_iap_transactions_transaction_id"),
|
|
sa.UniqueConstraint("ledger_event_id", name="uq_apple_iap_transactions_ledger_event_id"),
|
|
)
|
|
|
|
op.create_index(
|
|
"ix_apple_iap_transactions_user_created_at",
|
|
"apple_iap_transactions",
|
|
["user_id", sa.text("created_at DESC")],
|
|
)
|
|
op.create_index(
|
|
"ix_apple_iap_transactions_status_updated_at",
|
|
"apple_iap_transactions",
|
|
["status", sa.text("updated_at DESC")],
|
|
)
|
|
|
|
op.execute("ALTER TABLE apple_iap_transactions ENABLE ROW LEVEL SECURITY")
|
|
|
|
op.execute(
|
|
"CREATE POLICY anon_select_apple_iap_transactions ON apple_iap_transactions "
|
|
"FOR SELECT TO anon USING (false)"
|
|
)
|
|
op.execute(
|
|
"CREATE POLICY anon_insert_apple_iap_transactions ON apple_iap_transactions "
|
|
"FOR INSERT TO anon WITH CHECK (true)"
|
|
)
|
|
op.execute(
|
|
"CREATE POLICY anon_update_apple_iap_transactions ON apple_iap_transactions "
|
|
"FOR UPDATE TO anon USING (false)"
|
|
)
|
|
op.execute(
|
|
"CREATE POLICY anon_delete_apple_iap_transactions ON apple_iap_transactions "
|
|
"FOR DELETE TO anon USING (false)"
|
|
)
|
|
op.execute(
|
|
"CREATE POLICY authenticated_select_apple_iap_transactions ON apple_iap_transactions "
|
|
"FOR SELECT TO authenticated USING (false)"
|
|
)
|
|
op.execute(
|
|
"CREATE POLICY authenticated_insert_apple_iap_transactions ON apple_iap_transactions "
|
|
"FOR INSERT TO authenticated WITH CHECK (true)"
|
|
)
|
|
op.execute(
|
|
"CREATE POLICY authenticated_update_apple_iap_transactions ON apple_iap_transactions "
|
|
"FOR UPDATE TO authenticated USING (false)"
|
|
)
|
|
op.execute(
|
|
"CREATE POLICY authenticated_delete_apple_iap_transactions ON apple_iap_transactions "
|
|
"FOR DELETE TO authenticated USING (false)"
|
|
)
|
|
|
|
op.drop_constraint("ck_points_ledger_change_type", "points_ledger", type_="check")
|
|
op.create_check_constraint(
|
|
"ck_points_ledger_change_type",
|
|
"points_ledger",
|
|
"change_type in ('register', 'consume', 'adjust', 'purchase', 'refund')",
|
|
)
|
|
|
|
op.drop_constraint("ck_points_ledger_biz_type", "points_ledger", type_="check")
|
|
op.create_check_constraint(
|
|
"ck_points_ledger_biz_type",
|
|
"points_ledger",
|
|
"biz_type is null or biz_type in ('chat', 'payment')",
|
|
)
|
|
|
|
op.drop_constraint("ck_points_ledger_biz_binding", "points_ledger", type_="check")
|
|
op.create_check_constraint(
|
|
"ck_points_ledger_biz_binding",
|
|
"points_ledger",
|
|
"((change_type in ('register', 'adjust') and biz_type is null and biz_id is null) or "
|
|
"(change_type = 'consume' and biz_type = 'chat' and biz_id is not null) or "
|
|
"(change_type in ('purchase', 'refund') and biz_type = 'payment' and biz_id is not null))",
|
|
)
|
|
|
|
op.drop_constraint("ck_points_ledger_direction_by_change_type", "points_ledger", type_="check")
|
|
op.create_check_constraint(
|
|
"ck_points_ledger_direction_by_change_type",
|
|
"points_ledger",
|
|
"((change_type in ('register', 'purchase') and direction = 1) or "
|
|
"(change_type in ('consume', 'refund') and direction = -1) or "
|
|
"(change_type = 'adjust' and direction in (1, -1)))",
|
|
)
|
|
|
|
op.drop_constraint("ck_points_ledger_metadata_adjust_shape", "points_ledger", type_="check")
|
|
op.create_check_constraint(
|
|
"ck_points_ledger_metadata_adjust_shape",
|
|
"points_ledger",
|
|
"(change_type <> 'adjust' or ("
|
|
"(metadata ? 'ext') and (metadata->'ext' ? 'reason') and "
|
|
"coalesce(metadata #>> '{ext,reason}', '') <> ''))",
|
|
)
|
|
|
|
op.create_check_constraint(
|
|
"ck_points_ledger_metadata_payment_shape",
|
|
"points_ledger",
|
|
"(change_type not in ('purchase', 'refund') or ("
|
|
"(metadata ? 'ext') and (metadata->'ext' ? 'source') and (metadata->'ext' ? 'platform') and "
|
|
"(metadata->'ext' ? 'product_code') and (metadata->'ext' ? 'transaction_id') and "
|
|
"coalesce(metadata #>> '{ext,source}', '') <> '' and "
|
|
"coalesce(metadata #>> '{ext,platform}', '') <> '' and "
|
|
"coalesce(metadata #>> '{ext,product_code}', '') <> '' and "
|
|
"coalesce(metadata #>> '{ext,transaction_id}', '') <> ''))",
|
|
)
|
|
|
|
op.create_check_constraint(
|
|
"ck_points_ledger_metadata_refund_shape",
|
|
"points_ledger",
|
|
"(change_type <> 'refund' or ("
|
|
"(metadata ? 'ext') and (metadata->'ext' ? 'original_event_id') and "
|
|
"coalesce(metadata #>> '{ext,original_event_id}', '') <> ''))",
|
|
)
|
|
|
|
op.drop_constraint("ck_points_audit_ledger_change_type", "points_audit_ledger", type_="check")
|
|
op.create_check_constraint(
|
|
"ck_points_audit_ledger_change_type",
|
|
"points_audit_ledger",
|
|
"change_type in ('register', 'consume', 'adjust', 'purchase', 'refund')",
|
|
)
|
|
|
|
op.drop_constraint("ck_points_audit_ledger_biz_type", "points_audit_ledger", type_="check")
|
|
op.create_check_constraint(
|
|
"ck_points_audit_ledger_biz_type",
|
|
"points_audit_ledger",
|
|
"biz_type is null or biz_type in ('chat', 'payment')",
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_constraint("ck_points_audit_ledger_biz_type", "points_audit_ledger", type_="check")
|
|
op.create_check_constraint(
|
|
"ck_points_audit_ledger_biz_type",
|
|
"points_audit_ledger",
|
|
"biz_type is null or biz_type = 'chat'",
|
|
)
|
|
|
|
op.drop_constraint("ck_points_audit_ledger_change_type", "points_audit_ledger", type_="check")
|
|
op.create_check_constraint(
|
|
"ck_points_audit_ledger_change_type",
|
|
"points_audit_ledger",
|
|
"change_type in ('register', 'consume', 'grant', 'adjust')",
|
|
)
|
|
|
|
op.drop_constraint("ck_points_ledger_metadata_refund_shape", "points_ledger", type_="check")
|
|
op.drop_constraint("ck_points_ledger_metadata_payment_shape", "points_ledger", type_="check")
|
|
|
|
op.drop_constraint("ck_points_ledger_metadata_adjust_shape", "points_ledger", type_="check")
|
|
op.create_check_constraint(
|
|
"ck_points_ledger_metadata_adjust_shape",
|
|
"points_ledger",
|
|
"(change_type <> 'adjust' or ("
|
|
"(metadata ? 'ext') and (metadata->'ext' ? 'ticket_id') and "
|
|
"coalesce(metadata #>> '{ext,ticket_id}', '') <> ''))",
|
|
)
|
|
|
|
op.drop_constraint("ck_points_ledger_direction_by_change_type", "points_ledger", type_="check")
|
|
op.create_check_constraint(
|
|
"ck_points_ledger_direction_by_change_type",
|
|
"points_ledger",
|
|
"((change_type in ('register', 'grant') and direction = 1) or "
|
|
"(change_type = 'consume' and direction = -1) or "
|
|
"(change_type = 'adjust' and direction in (1, -1)))",
|
|
)
|
|
|
|
op.drop_constraint("ck_points_ledger_biz_binding", "points_ledger", type_="check")
|
|
op.create_check_constraint(
|
|
"ck_points_ledger_biz_binding",
|
|
"points_ledger",
|
|
"((change_type = 'register' and biz_type is null and biz_id is null) or "
|
|
"(change_type in ('consume', 'grant', 'adjust') and biz_type = 'chat' and biz_id is not null))",
|
|
)
|
|
|
|
op.drop_constraint("ck_points_ledger_biz_type", "points_ledger", type_="check")
|
|
op.create_check_constraint(
|
|
"ck_points_ledger_biz_type",
|
|
"points_ledger",
|
|
"biz_type is null or biz_type = 'chat'",
|
|
)
|
|
|
|
op.drop_constraint("ck_points_ledger_change_type", "points_ledger", type_="check")
|
|
op.create_check_constraint(
|
|
"ck_points_ledger_change_type",
|
|
"points_ledger",
|
|
"change_type in ('register', 'consume', 'grant', 'adjust')",
|
|
)
|
|
|
|
op.drop_index("ix_apple_iap_transactions_status_updated_at", table_name="apple_iap_transactions")
|
|
op.drop_index("ix_apple_iap_transactions_user_created_at", table_name="apple_iap_transactions")
|
|
|
|
op.execute("DROP POLICY IF EXISTS authenticated_delete_apple_iap_transactions ON apple_iap_transactions")
|
|
op.execute("DROP POLICY IF EXISTS authenticated_update_apple_iap_transactions ON apple_iap_transactions")
|
|
op.execute("DROP POLICY IF EXISTS authenticated_insert_apple_iap_transactions ON apple_iap_transactions")
|
|
op.execute("DROP POLICY IF EXISTS authenticated_select_apple_iap_transactions ON apple_iap_transactions")
|
|
op.execute("DROP POLICY IF EXISTS anon_delete_apple_iap_transactions ON apple_iap_transactions")
|
|
op.execute("DROP POLICY IF EXISTS anon_update_apple_iap_transactions ON apple_iap_transactions")
|
|
op.execute("DROP POLICY IF EXISTS anon_insert_apple_iap_transactions ON apple_iap_transactions")
|
|
op.execute("DROP POLICY IF EXISTS anon_select_apple_iap_transactions ON apple_iap_transactions")
|
|
|
|
op.execute("ALTER TABLE apple_iap_transactions DISABLE ROW LEVEL SECURITY")
|
|
|
|
op.drop_table("apple_iap_transactions")
|