71135c4e3a
start_play 无锁 check-then-insert,并发 /start 会算出同一个 seq、各拿一个 play_token 绕过次数上限刷金币;加 (user_id, seq) 唯一键,撞键即降级为不放视频。 grant_play 改成 status='playing'→'granted' 条件更新(compare-and-set), 播完与关闭抢跑/重试只会命中一条,不再二次铸币。含迁移与测试。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
41 lines
1.5 KiB
Python
41 lines
1.5 KiB
Python
"""guide_video_play 加 (user_id, seq) 唯一约束:堵住并发 /start 绕过次数上限
|
|
|
|
start_play 是无锁 check-then-insert(读 COUNT(*) 算 seq=used+1 再插一行),N 个并发
|
|
/start 会都读到同一个 used、算出同一个 seq、各插一行拿到各自的 play_token,于是 3 次
|
|
上限被绕过、每个 token 都能换 120 金币。加唯一键后并发同 seq 必撞,start_play 捕获
|
|
IntegrityError 降级为 should_play=false(客户端照旧放广告)。
|
|
|
|
用 unique index 而不是 batch_alter_table 加 UniqueConstraint:SQLite 加约束要整表重建,
|
|
而 CREATE UNIQUE INDEX 两边都原生支持,回滚也干净。
|
|
|
|
注:若库里已有并发产生的重复 (user_id, seq),建索引会失败 —— 本功能尚未上线,表通常是空的;
|
|
真撞上了先按 seq 去重(留 id 最小的一行,多发的金币按 scripts/reset_guide_video.py 的口径退)。
|
|
|
|
Revision ID: guide_video_user_seq_uq
|
|
Revises: d9c03cc3ea07
|
|
Create Date: 2026-07-24 10:00:00.000000
|
|
|
|
"""
|
|
from typing import Sequence, Union
|
|
|
|
from alembic import op
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision: str = "guide_video_user_seq_uq"
|
|
down_revision: Union[str, Sequence[str], None] = "d9c03cc3ea07"
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_index(
|
|
"uq_guide_video_play_user_seq",
|
|
"guide_video_play",
|
|
["user_id", "seq"],
|
|
unique=True,
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_index("uq_guide_video_play_user_seq", table_name="guide_video_play")
|