a2270ee1b2
新手引导视频:运营后台上传 MP4(上限 100MB,魔数校验只认 ISO BMFF), App 端在领券等候浮层前 N 次以引导视频替代广告。新增 guide_video 的 model/schema/repository/router(App 侧 + 后台侧)与播放记录表迁移。 美团券:首页「销量最高 / 智能推荐」两个 tab 改游标分页,配套两条 (city_id, dedup_key, 排序键 DESC) 复合索引,让 Postgres 顺着索引流式 去重,免掉每翻一页重排整城券的开销。美团 CPS client 在 lifespan 预热 并在关闭时释放连接池。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
63 lines
3.1 KiB
Python
63 lines
3.1 KiB
Python
"""新手引导视频播放记录(领券浮层前 N 次用它替代广告)。
|
|
|
|
产品规则(2026-07 拍板):新用户点「一键自动领取」后的等候浮层,**前 3 次**不放广告,
|
|
改放运营后台上传的引导视频;每次固定 120 金币,中途关闭也算看完照发。
|
|
|
|
口径:
|
|
- **计次按账号**(user_id),与设备无关 —— 换设备不重新送 3 次。
|
|
- **开播即计数**:客户端每次要展示浮层时调 `/api/v1/guide-video/start`,服务端当场
|
|
写一行(status='playing')并返回 play_token;`COUNT(*)` 即已用次数。用户中途 kill
|
|
App 也算用掉一次(产品选定口径,防反复进出刷金币)。
|
|
- **发币幂等**靠 play_token 唯一键:同一次播放重复上报只发一次。
|
|
|
|
与广告收益(ad_feed_reward_record)彻底分离:引导视频不是广告,不该进广告收益报表。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import DateTime, ForeignKey, Integer, String, UniqueConstraint, func
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from app.db.base import Base
|
|
|
|
|
|
class GuideVideoPlay(Base):
|
|
"""一次引导视频播放一行。开播时建(status='playing'),发币后置 'granted'。"""
|
|
|
|
__tablename__ = "guide_video_play"
|
|
__table_args__ = (
|
|
# 客户端幂等键:同一次播放重复上报奖励只发一次。
|
|
UniqueConstraint("play_token", name="uq_guide_video_play_token"),
|
|
)
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
user_id: Mapped[int] = mapped_column(
|
|
Integer, ForeignKey("user.id"), index=True, nullable=False
|
|
)
|
|
# 服务端生成下发给客户端的幂等键(uuid hex)。
|
|
play_token: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
# 触发场景:目前只有 coupon(领券等候浮层);留字段以便日后比价等场景复用。
|
|
scene: Mapped[str] = mapped_column(String(16), nullable=False, default="coupon")
|
|
# 本账号第几次(1-based),= 建行时已有行数 + 1。仅留痕/排查用,判定仍以 COUNT 为准。
|
|
seq: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
|
# 当次下发的视频地址(运营换片后能回溯用户当时看的是哪支)。
|
|
video_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
|
# 实发金币;未发时 0。
|
|
coin: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
# playing(已开播未发币) / granted(已发币)。
|
|
status: Mapped[str] = mapped_column(String(16), nullable=False, default="playing")
|
|
# 客户端上报时是否播完(true=自然播完 / false=中途关闭)。仅留痕:两者都发币。
|
|
completed: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
|
|
started_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), index=True, nullable=False
|
|
)
|
|
granted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
|
|
def __repr__(self) -> str: # pragma: no cover
|
|
return (
|
|
f"<GuideVideoPlay user={self.user_id} seq={self.seq} "
|
|
f"{self.status} coin={self.coin}>"
|
|
)
|