From 49e271927a4e31b4fda89dac262e5a1a6fffd2c2 Mon Sep 17 00:00:00 2001 From: unknown <798648091@qq.com> Date: Thu, 23 Jul 2026 21:50:19 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E9=A2=86=E5=88=B8?= =?UTF-8?q?=E5=8D=95=E5=88=B8=E6=88=90=E5=8A=9F=E7=8E=87=E6=8C=89=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E7=8B=AC=E7=AB=8B=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- alembic/versions/coupon_claim_event.py | 93 ++++++++++++ app/admin/repositories/coupon_data.py | 24 +-- app/admin/schemas/coupon_data.py | 4 +- app/api/v1/coupon.py | 4 +- app/models/__init__.py | 1 + app/models/coupon_state.py | 34 +++++ app/repositories/coupon_state.py | 41 ++++- scripts/seed_coupon_session_mock.py | 197 +++++++++++++++++++++++++ tests/test_coupon_claim_event.py | 54 +++++++ tests/test_coupon_point_score.py | 20 +-- tests/test_coupon_slots.py | 3 +- 11 files changed, 446 insertions(+), 29 deletions(-) create mode 100644 alembic/versions/coupon_claim_event.py create mode 100644 scripts/seed_coupon_session_mock.py create mode 100644 tests/test_coupon_claim_event.py diff --git a/alembic/versions/coupon_claim_event.py b/alembic/versions/coupon_claim_event.py new file mode 100644 index 0000000..8161330 --- /dev/null +++ b/alembic/versions/coupon_claim_event.py @@ -0,0 +1,93 @@ +"""add per-session coupon claim event table + +Revision ID: coupon_claim_event +Revises: 8e04cc13a211 +Create Date: 2026-07-23 +""" + +from collections.abc import Sequence + +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +from alembic import op + +revision: str = "coupon_claim_event" +down_revision: str | Sequence[str] | None = "8e04cc13a211" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + +_JSON = sa.JSON().with_variant(postgresql.JSONB(), "postgresql") + + +def upgrade() -> None: + op.create_table( + "coupon_claim_event", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("trace_id", sa.String(length=64), nullable=False), + sa.Column("device_id", sa.String(length=64), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=True), + sa.Column("coupon_id", sa.String(length=64), nullable=False), + sa.Column("claim_date", sa.Date(), nullable=False), + sa.Column("status", sa.String(length=24), nullable=False), + sa.Column("app_env", sa.String(length=16), nullable=True), + sa.Column("vendor", sa.String(length=48), nullable=True), + sa.Column("coupon_name", sa.String(length=128), nullable=True), + sa.Column("claimed_count", sa.Integer(), nullable=True), + sa.Column("reason", sa.String(length=255), nullable=True), + sa.Column("extra", _JSON, nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint( + "trace_id", "coupon_id", + name="uq_coupon_claim_event_trace_coupon", + ), + ) + op.create_index( + "ix_coupon_claim_event_date_env", + "coupon_claim_event", + ["claim_date", "app_env"], + unique=False, + ) + op.create_index( + op.f("ix_coupon_claim_event_app_env"), + "coupon_claim_event", + ["app_env"], + unique=False, + ) + op.create_index( + op.f("ix_coupon_claim_event_trace_id"), + "coupon_claim_event", + ["trace_id"], + unique=False, + ) + op.create_index( + op.f("ix_coupon_claim_event_user_id"), + "coupon_claim_event", + ["user_id"], + unique=False, + ) + + # 旧表只能回填当前仍保留的 trace;历史上已被每日去重覆盖的关联无法恢复。 + op.execute( + """ + INSERT INTO coupon_claim_event ( + trace_id, device_id, user_id, coupon_id, claim_date, status, app_env, + vendor, coupon_name, claimed_count, reason, extra, created_at, updated_at + ) + SELECT + trace_id, device_id, user_id, coupon_id, claim_date, status, app_env, + vendor, coupon_name, claimed_count, reason, extra, created_at, updated_at + FROM coupon_claim_record + WHERE trace_id IS NOT NULL + """ + ) + + +def downgrade() -> None: + op.drop_index(op.f("ix_coupon_claim_event_user_id"), table_name="coupon_claim_event") + op.drop_index(op.f("ix_coupon_claim_event_trace_id"), table_name="coupon_claim_event") + op.drop_index(op.f("ix_coupon_claim_event_app_env"), table_name="coupon_claim_event") + op.drop_index("ix_coupon_claim_event_date_env", table_name="coupon_claim_event") + op.drop_table("coupon_claim_event") diff --git a/app/admin/repositories/coupon_data.py b/app/admin/repositories/coupon_data.py index 5ae22d4..f7f8179 100644 --- a/app/admin/repositories/coupon_data.py +++ b/app/admin/repositories/coupon_data.py @@ -16,7 +16,7 @@ from sqlalchemy import case, func, or_, select from sqlalchemy.orm import Session from app.core import rewards -from app.models.coupon_state import CouponClaimRecord, CouponSession +from app.models.coupon_state import CouponClaimEvent, CouponClaimRecord, CouponSession from app.models.user import User from app.repositories import ad_ecpm as crud_ecpm from app.repositories.coupon_state import DEFAULT_PLATFORMS, coupon_id_to_platform @@ -193,18 +193,18 @@ def _point_scores_by_trace(db: Session, trace_ids: list[str]) -> dict[str, dict[ """聚合查询批量返回逐场点位分数,不加载逐券明细。""" if not trace_ids: return {} - succeeded = func.sum(case((CouponClaimRecord.status.in_(_SLOT_OK), 1), else_=0)) + succeeded = func.sum(case((CouponClaimEvent.status.in_(_SLOT_OK), 1), else_=0)) rows = db.execute( select( - CouponClaimRecord.trace_id, + CouponClaimEvent.trace_id, succeeded.label("succeeded"), func.count().label("tried"), ) .where( - CouponClaimRecord.trace_id.in_(trace_ids), - CouponClaimRecord.status.in_(_SLOT_TRIED), + CouponClaimEvent.trace_id.in_(trace_ids), + CouponClaimEvent.status.in_(_SLOT_TRIED), ) - .group_by(CouponClaimRecord.trace_id) + .group_by(CouponClaimEvent.trace_id) ).all() return { trace_id: {"succeeded": int(success_count or 0), "tried": int(tried or 0)} @@ -217,13 +217,13 @@ def coupon_point_details(db: Session, *, trace_id: str) -> list[dict]: """按单个 trace 查询逐券结果;仅在后台用户点击分数时调用。""" rows = db.execute( select( - CouponClaimRecord.coupon_id, - CouponClaimRecord.coupon_name, - CouponClaimRecord.status, - CouponClaimRecord.reason, + CouponClaimEvent.coupon_id, + CouponClaimEvent.coupon_name, + CouponClaimEvent.status, + CouponClaimEvent.reason, ) - .where(CouponClaimRecord.trace_id == trace_id) - .order_by(CouponClaimRecord.id) + .where(CouponClaimEvent.trace_id == trace_id) + .order_by(CouponClaimEvent.id) ).all() return [ { diff --git a/app/admin/schemas/coupon_data.py b/app/admin/schemas/coupon_data.py index f295650..6b19fd7 100644 --- a/app/admin/schemas/coupon_data.py +++ b/app/admin/schemas/coupon_data.py @@ -79,10 +79,10 @@ class CouponDataRow(BaseModel): started_at: datetime = Field(..., description="发起时刻(明细「时间」列)") claimed_count: int | None = None point_success_count: int | None = Field( - None, description="本次成功券点位数(success+already_claimed);无逐券埋点为空" + None, description="本次成功单券数(success+already_claimed);无逐券事件为空" ) point_total_count: int | None = Field( - None, description="本次尝试券点位数(success+already_claimed+failed,不含 skipped);无逐券埋点为空" + None, description="本次尝试单券数(success+already_claimed+failed,不含 skipped);无逐券事件为空" ) trace_url: str | None = Field(None, description="pricebot 公网 trace 链接(仅 completed 有);admin 渲染可点链接,无则显示可复制 trace_id") ad_revenue_yuan: float = Field( diff --git a/app/api/v1/coupon.py b/app/api/v1/coupon.py index bd2127a..ecd40a8 100644 --- a/app/api/v1/coupon.py +++ b/app/api/v1/coupon.py @@ -81,7 +81,7 @@ def _record_claims_blocking( device_id: str, user_id: int | None, trace_id: str | None, results: list[dict] ) -> None: with SessionLocal() as db: - # 取本次 session 环境,给 coupon_claim_record 打 app_env 标(每券成功率表按它过滤;设计 §13)。 + # 取本次 session 环境,给每日资产和逐次事件同时打环境标。 app_env = coupon_repo.session_app_env(db, trace_id) coupon_repo.record_claims(db, device_id, user_id, trace_id, results, app_env=app_env) # 顺带把本帧「成功平台」并入 coupon_session.platform_success(admin 领券数据 ②整单/③点位成功率; @@ -176,7 +176,7 @@ async def coupon_step( resp_json = resp.json() - # 领券结果沉淀(资产):中间帧 last_coupon_result + done 帧 coupon_results 幂等写库。 + # 领券结果沉淀:每日资产 + 逐次事件;中间帧和 done 全量帧均幂等写库。 # 当前只记录、不参与"要不要领"判断(MVP 先不去重)。写库失败不影响返回。 if device_id: results = _extract_coupon_results(resp_json) diff --git a/app/models/__init__.py b/app/models/__init__.py index 5ed8a6b..dd4f609 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -21,6 +21,7 @@ from app.models.cps_wx_user import CpsWxUser # noqa: F401 from app.models.comparison_milestone import ComparisonMilestoneClaim # noqa: F401 from app.models.device import DeviceLiveness # noqa: F401 from app.models.coupon_state import ( # noqa: F401 + CouponClaimEvent, CouponClaimRecord, CouponDailyCompletion, CouponPromptEngagement, diff --git a/app/models/coupon_state.py b/app/models/coupon_state.py index d461613..6d52c53 100644 --- a/app/models/coupon_state.py +++ b/app/models/coupon_state.py @@ -96,6 +96,40 @@ class CouponClaimRecord(Base): ) +class CouponClaimEvent(Base): + """一次领券任务中的单券结果,按 ``(trace_id, coupon_id)`` 幂等。""" + + __tablename__ = "coupon_claim_event" + __table_args__ = ( + UniqueConstraint( + "trace_id", "coupon_id", + name="uq_coupon_claim_event_trace_coupon", + ), + Index("ix_coupon_claim_event_date_env", "claim_date", "app_env"), + ) + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + trace_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True) + device_id: Mapped[str] = mapped_column(String(64), nullable=False) + user_id: Mapped[int | None] = mapped_column(Integer, index=True, nullable=True) + coupon_id: Mapped[str] = mapped_column(String(64), nullable=False) + claim_date: Mapped[date] = mapped_column(Date, nullable=False) + status: Mapped[str] = mapped_column(String(24), nullable=False) + app_env: Mapped[str | None] = mapped_column(String(16), index=True, nullable=True) + vendor: Mapped[str | None] = mapped_column(String(48), nullable=True) + coupon_name: Mapped[str | None] = mapped_column(String(128), nullable=True) + claimed_count: Mapped[int | None] = mapped_column(Integer, nullable=True) + reason: Mapped[str | None] = mapped_column(String(255), nullable=True) + extra: Mapped[dict | None] = mapped_column(_JSON, nullable=True) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), + nullable=False, + ) + + class CouponDailyCompletion(Base): """按 (device, 自然日) 记"今天是否已跑完整轮领券(到 done 帧)"——首页置灰源。 diff --git a/app/repositories/coupon_state.py b/app/repositories/coupon_state.py index 508da74..04ce173 100644 --- a/app/repositories/coupon_state.py +++ b/app/repositories/coupon_state.py @@ -14,6 +14,7 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from app.models.coupon_state import ( + CouponClaimEvent, CouponClaimRecord, CouponDailyCompletion, CouponPromptEngagement, @@ -164,11 +165,12 @@ def record_claims( results: list[dict], app_env: str | None = None, ) -> int: - """一批券领取结果幂等写入,返回写入(新增 + 更新)条数。 + """一批券领取结果同时写入每日资产表和逐次事件表。 results 单项取自 pricebot 的 last_coupon_result / done.coupon_results,识别字段: coupon_id(必需)/ status(必需)/ name / vendor / reason /(display_count)。 - (device, coupon_id, 今天) 唯一:重复上报同张券走更新(status 以最后一次为准)。 + - CouponClaimRecord 按 (device, coupon_id, 今天) 幂等,供每日资产口径使用。 + - CouponClaimEvent 按 (trace_id, coupon_id) 幂等,供 admin 逐场统计使用。 """ today = today_cn() written = 0 @@ -208,6 +210,41 @@ def record_claims( claimed_count=count, trace_id=trace_id, reason=r.get("reason"), extra=r, )) + if trace_id: + event = db.execute( + select(CouponClaimEvent).where( + CouponClaimEvent.trace_id == trace_id, + CouponClaimEvent.coupon_id == coupon_id, + ) + ).scalar_one_or_none() + if event is not None: + event.device_id = device_id + event.status = status + event.reason = r.get("reason") + event.vendor = r.get("vendor") + event.coupon_name = r.get("name") + event.extra = r + if user_id is not None: + event.user_id = user_id + if count is not None: + event.claimed_count = count + if app_env is not None: + event.app_env = app_env + else: + db.add(CouponClaimEvent( + trace_id=trace_id, + device_id=device_id, + user_id=user_id, + coupon_id=coupon_id, + claim_date=today, + status=status, + app_env=app_env, + vendor=r.get("vendor"), + coupon_name=r.get("name"), + claimed_count=count, + reason=r.get("reason"), + extra=r, + )) written += 1 if written == 0: return 0 diff --git a/scripts/seed_coupon_session_mock.py b/scripts/seed_coupon_session_mock.py new file mode 100644 index 0000000..7ce614b --- /dev/null +++ b/scripts/seed_coupon_session_mock.py @@ -0,0 +1,197 @@ +"""生成 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() diff --git a/tests/test_coupon_claim_event.py b/tests/test_coupon_claim_event.py new file mode 100644 index 0000000..98316eb --- /dev/null +++ b/tests/test_coupon_claim_event.py @@ -0,0 +1,54 @@ +"""逐次单券事件不能被同设备同日的每日去重记录串场。""" + +from sqlalchemy import delete, select + +from app.admin.repositories.coupon_data import _point_scores_by_trace, coupon_point_details +from app.db.session import SessionLocal +from app.models.coupon_state import CouponClaimEvent, CouponClaimRecord +from app.repositories.coupon_state import record_claims + + +def test_same_device_same_day_keeps_scores_for_each_trace() -> None: + db = SessionLocal() + device = "event-repeat-device" + first_trace = "event-repeat-first" + second_trace = "event-repeat-second" + first_results = [ + {"coupon_id": "mt-repeat", "name": "美团测试券", "status": "success"}, + {"coupon_id": "tb-repeat", "name": "淘宝测试券", "status": "failed"}, + ] + second_results = [ + {"coupon_id": "mt-repeat", "name": "美团测试券", "status": "already_claimed"}, + {"coupon_id": "tb-repeat", "name": "淘宝测试券", "status": "success"}, + ] + try: + record_claims( + db, device, None, first_trace, first_results, app_env="dev" + ) + record_claims( + db, device, None, second_trace, second_results, app_env="prod" + ) + + assets = db.execute( + select(CouponClaimRecord).where(CouponClaimRecord.device_id == device) + ).scalars().all() + assert len(assets) == 2 + assert {row.trace_id for row in assets} == {first_trace} + + events = db.execute( + select(CouponClaimEvent).where(CouponClaimEvent.device_id == device) + ).scalars().all() + assert len(events) == 4 + assert {row.trace_id for row in events} == {first_trace, second_trace} + + scores = _point_scores_by_trace(db, [first_trace, second_trace]) + assert scores[first_trace] == {"succeeded": 1, "tried": 2} + assert scores[second_trace] == {"succeeded": 2, "tried": 2} + assert [row["status"] for row in coupon_point_details( + db, trace_id=second_trace + )] == ["already_claimed", "success"] + finally: + db.execute(delete(CouponClaimEvent).where(CouponClaimEvent.device_id == device)) + db.execute(delete(CouponClaimRecord).where(CouponClaimRecord.device_id == device)) + db.commit() + db.close() diff --git a/tests/test_coupon_point_score.py b/tests/test_coupon_point_score.py index 5108fcf..e238f6d 100644 --- a/tests/test_coupon_point_score.py +++ b/tests/test_coupon_point_score.py @@ -13,7 +13,7 @@ from app.admin.repositories.coupon_data import ( ) from app.admin.security import create_admin_token from app.db.session import SessionLocal -from app.models.coupon_state import CouponClaimRecord, CouponSession +from app.models.coupon_state import CouponClaimEvent, CouponSession def test_point_scores_by_trace() -> None: @@ -22,14 +22,14 @@ def test_point_scores_by_trace() -> None: trace = "point-score-trace" try: db.add_all([ - CouponClaimRecord( + CouponClaimEvent( + trace_id=trace, device_id="score-device", coupon_id=f"mt-score-{status}", claim_date=date(2020, 1, 2), status=status, coupon_name=f"测试点位-{status}", reason="测试失败" if status == "failed" else None, - trace_id=trace, ) for status in ("success", "already_claimed", "failed", "skipped") ]) @@ -53,12 +53,12 @@ def test_skipped_detail_does_not_create_a_score() -> None: db = SessionLocal() trace = "point-score-skipped" try: - db.add(CouponClaimRecord( + db.add(CouponClaimEvent( + trace_id=trace, device_id="score-device-skipped", coupon_id="mt-score-skipped-only", claim_date=date(2020, 1, 2), status="skipped", - trace_id=trace, )) db.flush() @@ -87,12 +87,12 @@ def test_coupon_data_report_returns_scores_without_embedding_details() -> None: started_date=report_date, )) db.add_all([ - CouponClaimRecord( + CouponClaimEvent( + trace_id=trace, device_id="score-report-device", coupon_id=f"mt-report-{status}", claim_date=report_date, status=status, - trace_id=trace, ) for status in ("success", "failed") ]) @@ -128,14 +128,14 @@ def test_coupon_point_details_endpoint() -> None: role="super_admin", ) token, _expires_at = create_admin_token(admin_id=admin.id, role=admin.role) - db.add(CouponClaimRecord( + db.add(CouponClaimEvent( + trace_id=trace, device_id="point-details-endpoint-device", coupon_id="mt-point-details-endpoint", coupon_name="接口测试券", claim_date=date(2020, 1, 5), status="failed", reason="接口测试失败", - trace_id=trace, )) db.commit() @@ -156,6 +156,6 @@ def test_coupon_point_details_endpoint() -> None: } finally: db.rollback() - db.execute(delete(CouponClaimRecord).where(CouponClaimRecord.trace_id == trace)) + db.execute(delete(CouponClaimEvent).where(CouponClaimEvent.trace_id == trace)) db.commit() db.close() diff --git a/tests/test_coupon_slots.py b/tests/test_coupon_slots.py index b4f2a5b..10ce6d3 100644 --- a/tests/test_coupon_slots.py +++ b/tests/test_coupon_slots.py @@ -7,7 +7,7 @@ from sqlalchemy import delete, select from app.admin.repositories.coupon_data import coupon_slot_report from app.db.session import SessionLocal -from app.models.coupon_state import CouponClaimRecord, CouponSession +from app.models.coupon_state import CouponClaimEvent, CouponClaimRecord, CouponSession from app.repositories.coupon_state import record_claims, session_app_env @@ -49,6 +49,7 @@ def test_record_claims_stamps_app_env() -> None: assert row.app_env == "prod" assert row.status == "already_claimed" finally: + db.execute(delete(CouponClaimEvent).where(CouponClaimEvent.trace_id == "t-stamp")) db.execute(delete(CouponClaimRecord).where(CouponClaimRecord.device_id == dev)) db.commit() db.close() -- 2.52.0