Compare commits

...

1 Commits

Author SHA1 Message Date
unknown de24f08e58 修复:广告收益用户详情加载失败
用户奖励统计改为仅查询用户主键,避免完整用户模型依赖未同步字段;统一金币记录跨数据源的 UTC 排序键,修复 PostgreSQL 下 naive 与 aware 时间混排异常;补充旧表结构与混合时区回归测试。
2026-07-26 21:01:48 +08:00
4 changed files with 30 additions and 3 deletions
+9 -1
View File
@@ -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 datetimePostgreSQL 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 条)
+1 -1
View File
@@ -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)
+5
View File
@@ -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()
+15 -1
View File
@@ -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 返回 awareSQLite/历史转换可能返回 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))