"""生成 admin「领券记录」本地联调数据。 用法: python scripts/seed_coupon_session_mock.py 脚本只清理 ``mock-coupon-repeat-*`` 前缀的数据并重新生成。打开后台「领券记录」, 日期选今天;分别切换 prod/dev,可验证同一设备同一天多次领券仍各自显示正确分数。 """ from __future__ import annotations import sys from datetime import UTC, datetime from pathlib import Path from zoneinfo import ZoneInfo from sqlalchemy import delete, select ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from app.db.session import SessionLocal, engine # noqa: E402 from app.models.coupon_state import ( # noqa: E402 CouponClaimEvent, CouponClaimRecord, CouponSession, ) from app.models.user import User # noqa: E402 from app.repositories.coupon_state import record_claims, today_cn # noqa: E402 PREFIX = "mock-coupon-repeat-" CN_TZ = ZoneInfo("Asia/Shanghai") DEVICE_REPEAT = f"{PREFIX}device" DEVICE_CONTROL = f"{PREFIX}control-device" PHONE = "19900009001" USERNAME = "80000009001" PLATFORM_ELAPSED = { "meituan-waimai": 46_800, "taobao-shanguang": 19_500, "jd-waimai": 24_000, } FIRST_RESULTS = [ {"coupon_id": "mt_mock_1", "name": "美团模拟券1", "vendor": "meituan", "status": "success"}, {"coupon_id": "mt_mock_2", "name": "美团模拟券2", "vendor": "meituan", "status": "success"}, {"coupon_id": "mt_mock_3", "name": "美团模拟券3", "vendor": "meituan", "status": "success"}, {"coupon_id": "mt_mock_4", "name": "美团模拟券4", "vendor": "meituan", "status": "failed", "reason": "模拟失败"}, {"coupon_id": "tb_mock_1", "name": "淘宝模拟券1", "vendor": "taobao", "status": "success"}, {"coupon_id": "tb_mock_2", "name": "淘宝模拟券2", "vendor": "taobao", "status": "success"}, {"coupon_id": "tb_mock_3", "name": "淘宝模拟券3", "vendor": "taobao", "status": "success"}, {"coupon_id": "jd_mock_1", "name": "京东模拟券", "vendor": "jingdong", "status": "success"}, ] SECOND_RESULTS = [ {"coupon_id": "mt_mock_1", "name": "美团模拟券1", "vendor": "meituan", "status": "skipped", "reason": "模拟跳过,不计分母"}, {"coupon_id": "mt_mock_2", "name": "美团模拟券2", "vendor": "meituan", "status": "already_claimed"}, {"coupon_id": "mt_mock_3", "name": "美团模拟券3", "vendor": "meituan", "status": "success"}, {"coupon_id": "mt_mock_4", "name": "美团模拟券4", "vendor": "meituan", "status": "success"}, {"coupon_id": "tb_mock_1", "name": "淘宝模拟券1", "vendor": "taobao", "status": "success"}, {"coupon_id": "tb_mock_2", "name": "淘宝模拟券2", "vendor": "taobao", "status": "success"}, {"coupon_id": "tb_mock_3", "name": "淘宝模拟券3", "vendor": "taobao", "status": "already_claimed"}, {"coupon_id": "jd_mock_1", "name": "京东模拟券", "vendor": "jingdong", "status": "success"}, ] def _started_at(hour: int, minute: int) -> datetime: local = datetime.combine(today_cn(), datetime.min.time()).replace( hour=hour, minute=minute, tzinfo=CN_TZ ) return local.astimezone(UTC) def _session( *, trace_id: str, device_id: str, user_id: int, app_env: str, hour: int, minute: int, status: str = "completed", elapsed_ms: int | None = 91_900, platform_elapsed: dict[str, int] | None = None, ) -> CouponSession: started_at = _started_at(hour, minute) return CouponSession( trace_id=trace_id, device_id=device_id, user_id=user_id, status=status, app_env=app_env, platforms=[], origin_package=None, device_model="Mock Phone", rom="MockOS 1", started_at=started_at, started_date=today_cn(), finished_at=started_at if status != "started" else None, elapsed_ms=elapsed_ms, platform_elapsed=platform_elapsed, platform_success=( ["meituan-waimai", "taobao-shanguang", "jd-waimai"] if status == "completed" else None ), claimed_count=7 if status == "completed" else 0, ) def main() -> None: # 本地旧库 Alembic 版本链可能未同步;仅为联调补建新事件表,正式环境仍走 migration。 CouponClaimEvent.__table__.create(bind=engine, checkfirst=True) with SessionLocal() as db: db.execute(delete(CouponClaimEvent).where( CouponClaimEvent.trace_id.startswith(PREFIX) )) db.execute(delete(CouponClaimRecord).where( CouponClaimRecord.device_id.startswith(PREFIX) )) db.execute(delete(CouponSession).where( CouponSession.trace_id.startswith(PREFIX) )) user = db.execute(select(User).where(User.phone == PHONE)).scalar_one_or_none() if user is None: user = User( phone=PHONE, username=USERNAME, register_channel="sms", nickname="领券重复测试", ) db.add(user) db.flush() first_trace = f"{PREFIX}dev-first" second_trace = f"{PREFIX}prod-second" abandoned_trace = f"{PREFIX}prod-abandoned" control_trace = f"{PREFIX}prod-control" db.add_all([ _session( trace_id=first_trace, device_id=DEVICE_REPEAT, user_id=user.id, app_env="dev", hour=10, minute=0, platform_elapsed=PLATFORM_ELAPSED, ), _session( trace_id=second_trace, device_id=DEVICE_REPEAT, user_id=user.id, app_env="prod", hour=15, minute=0, platform_elapsed=PLATFORM_ELAPSED, ), _session( trace_id=abandoned_trace, device_id=DEVICE_REPEAT, user_id=user.id, app_env="prod", hour=16, minute=0, status="abandoned", elapsed_ms=21_500, platform_elapsed={"meituan-waimai": 20_500}, ), _session( trace_id=control_trace, device_id=DEVICE_CONTROL, user_id=user.id, app_env="prod", hour=17, minute=0, platform_elapsed=PLATFORM_ELAPSED, ), ]) db.commit() record_claims( db, DEVICE_REPEAT, user.id, first_trace, FIRST_RESULTS, app_env="dev" ) record_claims( db, DEVICE_REPEAT, user.id, second_trace, SECOND_RESULTS, app_env="prod" ) record_claims( db, DEVICE_CONTROL, user.id, control_trace, FIRST_RESULTS, app_env="prod" ) print(f"已生成 {today_cn()} 的领券 mock 数据。") print("筛选用户 19900009001。") print("prod 应有:7/7(100.0%)、-、7/8(87.5%)三条。") print("dev 应有:7/8(87.5%)一条。") if __name__ == "__main__": main()