"""金币→现金「每日自动兑」时区回归测试。 用户 2026-07-01 硬约束:不管服务器 / 用户设成什么时区,兑换一律按【北京时 0 点】日切。 本测试锁死这个行为,防以后有人把 rewards.cn_today() 改回 date.today() / 裸 datetime.now() (那会跟随进程本地时区,服务器非 CST 时在错误的"天"兑或重复兑)。 """ from __future__ import annotations from datetime import datetime, timezone from app.core import rewards from app.core.rewards import COIN_PER_CENT from app.db.session import SessionLocal from app.repositories import wallet as crud_wallet from app.repositories.user import get_user_by_phone def _login(client, phone: str) -> str: client.post("/api/v1/auth/sms/send", json={"phone": phone}) r = client.post("/api/v1/auth/sms/login", json={"phone": phone, "code": "123456"}) assert r.status_code == 200, r.text return r.json()["access_token"] def test_cn_today_is_beijing_date_regardless_of_utc_offset() -> None: """cn_today() 恒等于北京时的日期,与 UTC / 进程本地时区无关。 取一个 UTC 与北京跨天的时刻(北京已是次日 0 点后、UTC 还在前一天)验证: 2026-07-01 15:30 UTC = 2026-07-01 23:30 北京(同一天); 2026-07-01 17:30 UTC = 2026-07-02 01:30 北京(北京已跨天)。 """ cst = timezone(rewards.CN_TZ.utcoffset(None)) # UTC 17:30 → 北京次日 01:30:北京日期应是 07-02 moment_utc = datetime(2026, 7, 1, 17, 30, tzinfo=timezone.utc) assert moment_utc.astimezone(cst).date().isoformat() == "2026-07-02" # 反过来 UTC 23:30 → 北京 07:30 次日:北京日期 07-02(证明 +8 偏移生效) moment_utc2 = datetime(2026, 7, 1, 23, 30, tzinfo=timezone.utc) assert moment_utc2.astimezone(cst).date().isoformat() == "2026-07-02" # cn_today() 的实现就是 datetime.now(CN_TZ).date():与 UTC now 的北京投影一致 assert rewards.cn_today() == datetime.now(timezone.utc).astimezone(cst).date() def test_daily_auto_exchange_idempotent_within_same_beijing_day(client) -> None: """同一北京日内多次触发只兑一次(幂等键 = 当天是否已有 exchange_in 北京日流水)。""" token = _login(client, "13800007701") assert token with SessionLocal() as db: user = get_user_by_phone(db, "13800007701") assert user is not None # 给 200 金币(= 2 分),够到分兑换 crud_wallet.grant_coins(db, user.id, 200, biz_type="test_grant") db.commit() first = crud_wallet.daily_auto_exchange(db) assert first["converted"] == 1, first assert first["total_cents"] == 2, first # 当天再进账 200 金币,再触发一轮:因当天已有 exchange_in(北京日幂等键)→ 跳过, # 不重复兑(这 200 留到次日北京 0 点)。若幂等键被改成本地时区,服务器非 CST 时这里会误兑。 crud_wallet.grant_coins(db, user.id, 200, biz_type="test_grant") db.commit() second = crud_wallet.daily_auto_exchange(db) assert second["converted"] == 0, second assert second["skipped_done"] >= 1, second def test_has_exchange_in_on_keyed_on_given_beijing_day(client) -> None: """_has_exchange_in_on 以传入的北京日为界比较 [当天0点, 次日0点)。""" token = _login(client, "13800007702") assert token with SessionLocal() as db: user = get_user_by_phone(db, "13800007702") assert user is not None crud_wallet.grant_coins(db, user.id, 200, biz_type="test_grant") db.commit() crud_wallet.daily_auto_exchange(db) today = rewards.cn_today() assert crud_wallet._has_exchange_in_on(db, user.id, today) is True # 昨天(北京日)不该命中今天兑的流水 yesterday = today.fromordinal(today.toordinal() - 1) assert crud_wallet._has_exchange_in_on(db, user.id, yesterday) is False