From 84251770b45526fcceb0d28e23ff8b44bf527d21 Mon Sep 17 00:00:00 2001 From: linkeyu Date: Sat, 1 Aug 2026 21:35:06 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E9=A6=96=E9=A1=B5?= =?UTF-8?q?=E8=BD=AE=E6=92=AD=E6=8E=92=E9=99=A4=E6=B5=8B=E8=AF=95=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E6=95=B0=E6=8D=AE=20(#215)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 背景 线上首页轮播候选记录中,测试账号 u33(11111111111)贡献约 51.8% 的真实记录,导致该用户频繁出现。 ## 改动 - App 首页轮播真实记录查询排除全部已配置测试账号 - 后台首页轮播可展示记录同步采用相同过滤口径 - 同时兼容 TEST_ACCOUNT_PHONE 与 TEST_ACCOUNT_PHONES - 测试账号配置变化时立即使轮播查询缓存失效 - 不删除历史业务数据,仅在展示查询中排除 ## 验证 - ruff check:通过 - pytest tests/test_ops_marquee.py -q:1 passed --------- Co-authored-by: linkeyu <798648091@qq.com> Reviewed-on: https://gitea.shaguabijia.com/WonderableAI/shaguabijia-app-server/pulls/215 Co-authored-by: linkeyu Co-committed-by: linkeyu --- app/repositories/ops_marquee.py | 28 +++++++++--- tests/test_ops_marquee.py | 77 +++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 tests/test_ops_marquee.py diff --git a/app/repositories/ops_marquee.py b/app/repositories/ops_marquee.py index be5c1d3..293d6b6 100644 --- a/app/repositories/ops_marquee.py +++ b/app/repositories/ops_marquee.py @@ -25,6 +25,7 @@ from datetime import datetime, timedelta from sqlalchemy import delete, func, select, update from sqlalchemy.orm import Session +from app.core.config import settings from app.core.rewards import CN_TZ from app.models.comparison import ComparisonRecord from app.models.ops_marquee_seed import OpsMarqueeSeed @@ -54,7 +55,7 @@ _SEED_MAX_CENTS = 100000 # 展示层随机(抽样/金额/时间/名字合成)仍每次重算,缓存只省查询;新记录最多晚 30s 进轮播,可接受。 _REAL_ROWS_TTL_SECONDS = 30 _REAL_ROWS_FETCH_CAP = 600 # 一次多取些,够 limit≤30 去重后取数;命中缓存后复用 -_real_rows_cache: dict = {"at": None, "rows": None} +_real_rows_cache: dict = {"at": None, "rows": None, "test_phones": None} # ===== 用户标识脱敏(对齐 PRD) + 种子无真实昵称时的假名合成 ===== # 脱敏规则(按字符数,中英文皆适用):有昵称→n≥5「首+***+末」、n=4「首+**+末」、n≤3「首+**」; @@ -198,10 +199,16 @@ def _recent_real_rows(db: Session) -> list[tuple[int, int, str | None]]: 返回纯元组(脱离 session),可安全跨请求复用。极端并发下偶尔多查一次(无锁、幂等),纯门面无副作用。 """ now = datetime.now(CN_TZ) + test_phones = tuple(sorted(settings.test_account_phones)) cached, at = _real_rows_cache["rows"], _real_rows_cache["at"] - if cached is not None and at is not None and (now - at).total_seconds() < _REAL_ROWS_TTL_SECONDS: + if ( + cached is not None + and at is not None + and _real_rows_cache["test_phones"] == test_phones + and (now - at).total_seconds() < _REAL_ROWS_TTL_SECONDS + ): return cached - rows = db.execute( + stmt = ( select(ComparisonRecord.user_id, ComparisonRecord.saved_amount_cents, User.nickname) .join(User, User.id == ComparisonRecord.user_id) .where( @@ -211,9 +218,12 @@ def _recent_real_rows(db: Session) -> list[tuple[int, int, str | None]]: ) .order_by(ComparisonRecord.created_at.desc()) .limit(_REAL_ROWS_FETCH_CAP) - ).all() + ) + if test_phones: + stmt = stmt.where(User.phone.not_in(test_phones)) + rows = db.execute(stmt).all() out = [(int(uid), int(sc), nick) for uid, sc, nick in rows] - _real_rows_cache["rows"], _real_rows_cache["at"] = out, now + _real_rows_cache.update(rows=out, at=now, test_phones=test_phones) return out @@ -340,7 +350,7 @@ def list_real_records( # pool: [(cluster_key, item)];cluster_key 供去连簇——真实=user_id、种子=各自唯一负数(互不聚簇) pool: list[tuple[int, dict]] = [] if mode != "seed": - rows = db.execute( + stmt = ( select( ComparisonRecord.user_id, ComparisonRecord.saved_amount_cents, @@ -355,7 +365,11 @@ def list_real_records( ) .order_by(ComparisonRecord.created_at.desc()) .limit(_REAL_BROWSE_CAP) - ).all() + ) + test_phones = tuple(sorted(settings.test_account_phones)) + if test_phones: + stmt = stmt.where(User.phone.not_in(test_phones)) + rows = db.execute(stmt).all() for uid, sc, nick, ca in rows: pool.append(( int(uid), diff --git a/tests/test_ops_marquee.py b/tests/test_ops_marquee.py new file mode 100644 index 0000000..637506b --- /dev/null +++ b/tests/test_ops_marquee.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +from app.core.config import settings +from app.db.session import SessionLocal +from app.models.comparison import ComparisonRecord +from app.models.user import User +from app.repositories import ops_marquee + + +def _clear_real_rows_cache() -> None: + ops_marquee._real_rows_cache.update(at=None, rows=None, test_phones=None) + + +def test_real_marquee_records_exclude_configured_test_account(monkeypatch) -> None: + legacy_test_phone = "19900009991" + listed_test_phone = "19900009992" + real_phone = "19900009993" + monkeypatch.setattr(settings, "TEST_ACCOUNT_PHONE", legacy_test_phone) + monkeypatch.setattr(settings, "TEST_ACCOUNT_PHONES", listed_test_phone) + _clear_real_rows_cache() + + try: + with SessionLocal() as db: + legacy_test_user = User( + phone=legacy_test_phone, + username="29900009991", + register_channel="sms", + ) + listed_test_user = User( + phone=listed_test_phone, + username="29900009992", + register_channel="sms", + ) + real_user = User( + phone=real_phone, + username="29900009993", + register_channel="sms", + ) + db.add_all([legacy_test_user, listed_test_user, real_user]) + db.flush() + db.add_all([ + ComparisonRecord( + user_id=legacy_test_user.id, + trace_id="marquee-legacy-test-account-trace", + status="success", + saved_amount_cents=29_991, + items=[], + comparison_results=[], + ), + ComparisonRecord( + user_id=listed_test_user.id, + trace_id="marquee-listed-test-account-trace", + status="success", + saved_amount_cents=29_992, + items=[], + comparison_results=[], + ), + ComparisonRecord( + user_id=real_user.id, + trace_id="marquee-real-account-trace", + status="success", + saved_amount_cents=29_993, + items=[], + comparison_results=[], + ), + ]) + db.flush() + + app_rows = ops_marquee._recent_real_rows(db) + browse_rows, _ = ops_marquee.list_real_records(db, mode="real", limit=1_000) + finally: + _clear_real_rows_cache() + + assert all(saved not in {29_991, 29_992} for _uid, saved, _nickname in app_rows) + assert any(saved == 29_993 for _uid, saved, _nickname in app_rows) + assert all(row["saved_amount_cents"] not in {29_991, 29_992} for row in browse_rows) + assert any(row["saved_amount_cents"] == 29_993 for row in browse_rows)