Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de24f08e58 |
@@ -1228,6 +1228,11 @@ def _cn_wall_to_utc(dt: datetime) -> datetime:
|
||||
return dt.replace(tzinfo=rewards.CN_TZ).astimezone(timezone.utc).replace(tzinfo=None)
|
||||
|
||||
|
||||
def _coin_record_sort_key(row: dict) -> datetime:
|
||||
"""金币明细跨数据源排序键:兼容 SQLite naive 与 PostgreSQL aware 时间。"""
|
||||
return _as_utc(row["created_at"])
|
||||
|
||||
|
||||
def user_coin_records(
|
||||
db: Session,
|
||||
user_id: int,
|
||||
@@ -1319,7 +1324,10 @@ def user_coin_records(
|
||||
"coin": rec.amount,
|
||||
})
|
||||
|
||||
rows.sort(key=lambda r: r["created_at"], reverse=True)
|
||||
# SQLite 常返回 naive datetime,PostgreSQL timestamptz 返回 aware datetime;
|
||||
# 统一成 aware UTC 排序,避免线上合并广告记录与签到记录时抛
|
||||
# “can't compare offset-naive and offset-aware datetimes”。
|
||||
rows.sort(key=_coin_record_sort_key, reverse=True)
|
||||
has_more = len(rows) > offset + limit
|
||||
|
||||
# 总数 = 三源在窗口内 granted 计数之和(供前端页码分页渲染页码/共 N 条)
|
||||
|
||||
@@ -86,7 +86,7 @@ def get_user_reward_stats(
|
||||
date_to: Annotated[datetime | None, Query()] = None,
|
||||
) -> UserRewardStats:
|
||||
"""提现详情抽屉「用户统计区」。date_from/date_to 都不传 = 注册至今(全量)。"""
|
||||
if user_repo.get_user_by_id(db, user_id) is None:
|
||||
if not user_repo.user_exists(db, user_id):
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
return UserRewardStats(
|
||||
**queries.user_reward_stats(db, user_id, date_from=date_from, date_to=date_to)
|
||||
|
||||
@@ -93,6 +93,11 @@ def get_user_by_id(db: Session, user_id: int) -> User | None:
|
||||
return db.get(User, user_id)
|
||||
|
||||
|
||||
def user_exists(db: Session, user_id: int) -> bool:
|
||||
"""只查主键判断用户是否存在,避免只读统计接口依赖完整用户表结构。"""
|
||||
return db.scalar(select(User.id).where(User.id == user_id)) is not None
|
||||
|
||||
|
||||
def get_user_by_phone(db: Session, phone: str) -> User | None:
|
||||
stmt = select(User).where(User.phone == phone)
|
||||
return db.execute(stmt).scalar_one_or_none()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Admin M2 读接口测试:大盘聚合 + 用户/流水/提现/反馈列表 + 鉴权拦截。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
@@ -9,6 +9,7 @@ from sqlalchemy import event
|
||||
|
||||
from app.admin.main import admin_app
|
||||
from app.admin.repositories import admin_user as admin_repo
|
||||
from app.admin.repositories import queries
|
||||
from app.db.session import SessionLocal, engine
|
||||
from app.models.comparison import ComparisonRecord
|
||||
from app.models.feedback import Feedback
|
||||
@@ -144,6 +145,8 @@ def test_user_reward_detail_does_not_select_unrelated_new_ad_columns(
|
||||
) -> None:
|
||||
if "ad_reward_record.boost_round_id" in statement:
|
||||
raise AssertionError("提现详情不应查询未使用的 boost_round_id")
|
||||
if "FROM user" in statement and "user.phone" in statement:
|
||||
raise AssertionError("奖励统计的用户存在性检查不应展开完整 user 表")
|
||||
|
||||
event.listen(engine, "before_cursor_execute", reject_full_ad_reward_projection)
|
||||
try:
|
||||
@@ -162,6 +165,17 @@ def test_user_reward_detail_does_not_select_unrelated_new_ad_columns(
|
||||
assert records.status_code == 200, records.text
|
||||
|
||||
|
||||
def test_user_coin_record_sort_accepts_mixed_timezone_datetimes() -> None:
|
||||
"""线上 PostgreSQL 返回 aware,SQLite/历史转换可能返回 naive,二者必须可混排。"""
|
||||
naive = datetime(2038, 1, 1, 8, 0)
|
||||
aware = datetime(2038, 1, 1, 7, 0, tzinfo=UTC)
|
||||
|
||||
rows = [{"created_at": aware}, {"created_at": naive}]
|
||||
rows.sort(key=queries._coin_record_sort_key, reverse=True)
|
||||
|
||||
assert rows == [{"created_at": naive}, {"created_at": aware}]
|
||||
|
||||
|
||||
def test_user_filter_by_status(admin_client: TestClient, admin_token: str) -> None:
|
||||
_seed_user_with_data("13800000003")
|
||||
r = admin_client.get("/admin/api/users", params={"status": "active"}, headers=_auth(admin_token))
|
||||
|
||||
Reference in New Issue
Block a user