25b2b6850b
- 加 POST /api/v1/coupon/prompt/reset:删该设备今日 engagement → has_engaged_today 变 false、 今天又能弹。开发设置「重置今日领券弹窗状态」按钮调它(客户端改纯后台频控后前台清缓存已失效) - coupon_state 加 reset_today_engagement(按 device_id + 今天删);should-show 文档改为"纯后台判据" Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Reviewed-on: #29
143 lines
4.9 KiB
Python
143 lines
4.9 KiB
Python
"""领券今日状态读写:弹窗频控(engagement)+ 领券记录(claim)。
|
|
|
|
写操作按唯一键幂等 upsert,自带 commit + 并发 IntegrityError 兜底(对齐 price_observation)。
|
|
日期口径 = Asia/Shanghai 的自然日。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import date, datetime
|
|
from zoneinfo import ZoneInfo
|
|
|
|
from sqlalchemy import delete, select
|
|
from sqlalchemy.exc import IntegrityError
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.models.coupon_state import CouponClaimRecord, CouponPromptEngagement
|
|
|
|
logger = logging.getLogger("shagua.coupon_state")
|
|
|
|
_CN_TZ = ZoneInfo("Asia/Shanghai")
|
|
|
|
|
|
def today_cn() -> date:
|
|
"""Asia/Shanghai 的自然日(领券判断的"今天")。"""
|
|
return datetime.now(_CN_TZ).date()
|
|
|
|
|
|
# ===== 弹窗频控(coupon_prompt_engagement)=====
|
|
|
|
def has_engaged_today(db: Session, device_id: str) -> bool:
|
|
"""这台设备今天是否已对领券引导窗表达过意向(领或拒)。有 = 不再弹。"""
|
|
row = db.execute(
|
|
select(CouponPromptEngagement.id).where(
|
|
CouponPromptEngagement.device_id == device_id,
|
|
CouponPromptEngagement.engage_date == today_cn(),
|
|
)
|
|
).first()
|
|
return row is not None
|
|
|
|
|
|
def mark_engagement(
|
|
db: Session, device_id: str, user_id: int | None, engage_type: str
|
|
) -> None:
|
|
"""记今日意向(claim_started / dismissed)。(device, 今天) 唯一,幂等 upsert。"""
|
|
today = today_cn()
|
|
row = db.execute(
|
|
select(CouponPromptEngagement).where(
|
|
CouponPromptEngagement.device_id == device_id,
|
|
CouponPromptEngagement.engage_date == today,
|
|
)
|
|
).scalar_one_or_none()
|
|
if row is not None:
|
|
row.engage_type = engage_type
|
|
if user_id is not None:
|
|
row.user_id = user_id
|
|
else:
|
|
db.add(CouponPromptEngagement(
|
|
device_id=device_id, user_id=user_id,
|
|
engage_date=today, engage_type=engage_type,
|
|
))
|
|
try:
|
|
db.commit()
|
|
except IntegrityError:
|
|
# 并发下另一请求刚插了同 (device, 日) → 唯一约束撞,回滚忽略(本就幂等)。
|
|
db.rollback()
|
|
|
|
|
|
def reset_today_engagement(db: Session, device_id: str) -> int:
|
|
"""删这台设备今天的 engagement(开发设置「重置今日领券弹窗状态」调,测频控用)。
|
|
删后 has_engaged_today → false,今天又能弹。返回删除行数。"""
|
|
result = db.execute(
|
|
delete(CouponPromptEngagement).where(
|
|
CouponPromptEngagement.device_id == device_id,
|
|
CouponPromptEngagement.engage_date == today_cn(),
|
|
)
|
|
)
|
|
db.commit()
|
|
return result.rowcount or 0
|
|
|
|
|
|
# ===== 领券记录(coupon_claim_record)=====
|
|
|
|
def record_claims(
|
|
db: Session,
|
|
device_id: str,
|
|
user_id: int | None,
|
|
trace_id: str | None,
|
|
results: list[dict],
|
|
) -> int:
|
|
"""一批券领取结果幂等写入,返回写入(新增 + 更新)条数。
|
|
|
|
results 单项取自 pricebot 的 last_coupon_result / done.coupon_results,识别字段:
|
|
coupon_id(必需)/ status(必需)/ name / vendor / reason /(display_count)。
|
|
(device, coupon_id, 今天) 唯一:重复上报同张券走更新(status 以最后一次为准)。
|
|
"""
|
|
today = today_cn()
|
|
written = 0
|
|
seen: set[str] = set() # 同批去重防御:autoflush=False 下同 coupon_id 重复会两次 add → 撞唯一约束回滚整批
|
|
for r in results:
|
|
coupon_id = r.get("coupon_id")
|
|
status = r.get("status")
|
|
if not coupon_id or not status or coupon_id in seen:
|
|
continue # 脏数据 / 同批重复跳过
|
|
seen.add(coupon_id)
|
|
count = r.get("display_count")
|
|
if count is None:
|
|
count = r.get("claimed_count")
|
|
row = db.execute(
|
|
select(CouponClaimRecord).where(
|
|
CouponClaimRecord.device_id == device_id,
|
|
CouponClaimRecord.coupon_id == coupon_id,
|
|
CouponClaimRecord.claim_date == today,
|
|
)
|
|
).scalar_one_or_none()
|
|
if row is not None:
|
|
row.status = status
|
|
row.reason = r.get("reason")
|
|
if user_id is not None:
|
|
row.user_id = user_id
|
|
if count is not None:
|
|
row.claimed_count = count
|
|
row.extra = r
|
|
else:
|
|
db.add(CouponClaimRecord(
|
|
device_id=device_id, user_id=user_id,
|
|
coupon_id=coupon_id, claim_date=today,
|
|
status=status, vendor=r.get("vendor"), coupon_name=r.get("name"),
|
|
claimed_count=count, trace_id=trace_id, reason=r.get("reason"),
|
|
extra=r,
|
|
))
|
|
written += 1
|
|
if written == 0:
|
|
return 0
|
|
try:
|
|
db.commit()
|
|
except IntegrityError:
|
|
db.rollback()
|
|
logger.warning(
|
|
"coupon_claim 并发幂等冲突 device=%s trace=%s,回滚", device_id, trace_id
|
|
)
|
|
return 0
|
|
return written
|