"""add coin_transaction task ref unique index 可重复任务(task_enable_notification「打开消息提醒」逐次减半领取)的发放路径是无锁的 读-算-写:并发/连点的两个 claim 会都读到同一已领次数、算出同一 ref_id(task_key:N)、 各发一笔,导致双倍发钱。给 coin_transaction 加 (user_id, biz_type, ref_id) 部分唯一索引 (仅 biz_type LIKE 'task%' 且 ref_id 非空),第二笔撞唯一约束 → IntegrityError,被 task.claim_task 兜底成 AlreadyClaimedError(409)。与 cash_transaction 提现退款、 withdraw_order 在途单的「唯一约束 + IntegrityError 兜底」同模式。 Revision ID: coin_txn_task_ref_uq Revises: drop_force_onboarding Create Date: 2026-06-12 00:00:00.000000 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision: str = "coin_txn_task_ref_uq" down_revision: Union[str, Sequence[str], None] = "drop_force_onboarding" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: op.create_index( "ux_coin_transaction_task_ref", "coin_transaction", ["user_id", "biz_type", "ref_id"], unique=True, sqlite_where=sa.text("biz_type LIKE 'task%' AND ref_id IS NOT NULL"), postgresql_where=sa.text("biz_type LIKE 'task%' AND ref_id IS NOT NULL"), ) def downgrade() -> None: op.drop_index("ux_coin_transaction_task_ref", table_name="coin_transaction")