Compare commits

...

1 Commits

Author SHA1 Message Date
unknown c72e052528 修复:提现详情兼容旧数据库字段 2026-07-24 11:59:48 +08:00
2 changed files with 60 additions and 13 deletions
+29 -12
View File
@@ -1171,24 +1171,30 @@ def user_reward_stats(
acc = db.get(CoinAccount, user_id) # 现金余额:当前快照,不随窗口 acc = db.get(CoinAccount, user_id) # 现金余额:当前快照,不随窗口
cash_balance = acc.cash_balance_cents if acc else 0 cash_balance = acc.cash_balance_cents if acc else 0
rv = list(db.execute( # 只投影本统计实际使用的列。避免滚动发布或旧本地库尚未补齐无关新列时,
select(AdRewardRecord).where( # SQLAlchemy 因 select(ORM) 自动展开整表字段而让提现详情整体 500。
rv = db.execute(
select(AdRewardRecord.ecpm_raw, AdRewardRecord.coin).where(
AdRewardRecord.user_id == user_id, AdRewardRecord.user_id == user_id,
AdRewardRecord.reward_scene == "reward_video", AdRewardRecord.reward_scene == "reward_video",
AdRewardRecord.status == "granted", AdRewardRecord.status == "granted",
*_window_conds(AdRewardRecord.created_at, date_from, date_to), *_window_conds(AdRewardRecord.created_at, date_from, date_to),
) )
).scalars()) ).all()
rv_ecpms = [rewards.parse_ecpm_fen(r.ecpm_raw) for r in rv if r.ecpm_raw] rv_ecpms = [rewards.parse_ecpm_fen(r.ecpm_raw) for r in rv if r.ecpm_raw]
rv_coins = sum(r.coin for r in rv) rv_coins = sum(r.coin for r in rv)
feed = list(db.execute( feed = db.execute(
select(AdFeedRewardRecord).where( select(
AdFeedRewardRecord.unit_count,
AdFeedRewardRecord.ecpm_raw,
AdFeedRewardRecord.coin,
).where(
AdFeedRewardRecord.user_id == user_id, AdFeedRewardRecord.user_id == user_id,
AdFeedRewardRecord.status == "granted", AdFeedRewardRecord.status == "granted",
*_window_conds(AdFeedRewardRecord.created_at, date_from, date_to), *_window_conds(AdFeedRewardRecord.created_at, date_from, date_to),
) )
).scalars()) ).all()
feed_ecpms = [rewards.parse_ecpm_fen(f.ecpm_raw) for f in feed if f.ecpm_raw] feed_ecpms = [rewards.parse_ecpm_fen(f.ecpm_raw) for f in feed if f.ecpm_raw]
feed_coins = sum(f.coin for f in feed) feed_coins = sum(f.coin for f in feed)
@@ -1246,8 +1252,14 @@ def user_coin_records(
signin_from = date_from + timedelta(hours=8) if date_from is not None else None signin_from = date_from + timedelta(hours=8) if date_from is not None else None
signin_to = date_to + timedelta(hours=8) if date_to is not None else None signin_to = date_to + timedelta(hours=8) if date_to is not None else None
# 三类来源都只取页面需要的列,避免无关 ORM 新列造成旧库查询失败。
for rec in db.execute( for rec in db.execute(
select(AdRewardRecord) select(
AdRewardRecord.reward_scene,
AdRewardRecord.created_at,
AdRewardRecord.ecpm_raw,
AdRewardRecord.coin,
)
.where( .where(
AdRewardRecord.user_id == user_id, AdRewardRecord.user_id == user_id,
AdRewardRecord.status == "granted", AdRewardRecord.status == "granted",
@@ -1255,7 +1267,7 @@ def user_coin_records(
) )
.order_by(AdRewardRecord.created_at.desc()) .order_by(AdRewardRecord.created_at.desc())
.limit(fetch) .limit(fetch)
).scalars(): ).all():
is_video = rec.reward_scene == "reward_video" is_video = rec.reward_scene == "reward_video"
rows.append({ rows.append({
"source": rec.reward_scene, "source": rec.reward_scene,
@@ -1266,7 +1278,12 @@ def user_coin_records(
}) })
for rec in db.execute( for rec in db.execute(
select(AdFeedRewardRecord) select(
AdFeedRewardRecord.feed_scene,
AdFeedRewardRecord.created_at,
AdFeedRewardRecord.ecpm_raw,
AdFeedRewardRecord.coin,
)
.where( .where(
AdFeedRewardRecord.user_id == user_id, AdFeedRewardRecord.user_id == user_id,
AdFeedRewardRecord.status == "granted", AdFeedRewardRecord.status == "granted",
@@ -1274,7 +1291,7 @@ def user_coin_records(
) )
.order_by(AdFeedRewardRecord.created_at.desc()) .order_by(AdFeedRewardRecord.created_at.desc())
.limit(fetch) .limit(fetch)
).scalars(): ).all():
rows.append({ rows.append({
"source": "feed", "source": "feed",
"source_label": _FEED_SCENE_LABEL.get(rec.feed_scene, "信息流广告"), "source_label": _FEED_SCENE_LABEL.get(rec.feed_scene, "信息流广告"),
@@ -1284,7 +1301,7 @@ def user_coin_records(
}) })
for rec in db.execute( for rec in db.execute(
select(CoinTransaction) select(CoinTransaction.created_at, CoinTransaction.amount)
.where( .where(
CoinTransaction.user_id == user_id, CoinTransaction.user_id == user_id,
CoinTransaction.biz_type == "signin", CoinTransaction.biz_type == "signin",
@@ -1292,7 +1309,7 @@ def user_coin_records(
) )
.order_by(CoinTransaction.created_at.desc()) .order_by(CoinTransaction.created_at.desc())
.limit(fetch) .limit(fetch)
).scalars(): ).all():
rows.append({ rows.append({
"source": "signin", "source": "signin",
"source_label": "签到", "source_label": "签到",
+31 -1
View File
@@ -5,10 +5,11 @@ from datetime import datetime
import pytest import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from sqlalchemy import event
from app.admin.main import admin_app from app.admin.main import admin_app
from app.admin.repositories import admin_user as admin_repo from app.admin.repositories import admin_user as admin_repo
from app.db.session import SessionLocal from app.db.session import SessionLocal, engine
from app.models.comparison import ComparisonRecord from app.models.comparison import ComparisonRecord
from app.models.feedback import Feedback from app.models.feedback import Feedback
from app.models.wallet import CashTransaction, WithdrawOrder from app.models.wallet import CashTransaction, WithdrawOrder
@@ -132,6 +133,35 @@ def test_user_list_and_detail(admin_client: TestClient, admin_token: str) -> Non
assert admin_client.get("/admin/api/users/999999", headers=_auth(admin_token)).status_code == 404 assert admin_client.get("/admin/api/users/999999", headers=_auth(admin_token)).status_code == 404
def test_user_reward_detail_does_not_select_unrelated_new_ad_columns(
admin_client: TestClient, admin_token: str
) -> None:
"""旧库缺少无关新列时,提现详情的统计和金币记录仍应可读。"""
uid = _seed_user_with_data("13800000022")
def reject_full_ad_reward_projection(
_conn, _cursor, statement: str, _parameters, _context, _executemany
) -> None:
if "ad_reward_record.boost_round_id" in statement:
raise AssertionError("提现详情不应查询未使用的 boost_round_id")
event.listen(engine, "before_cursor_execute", reject_full_ad_reward_projection)
try:
stats = admin_client.get(
f"/admin/api/users/{uid}/reward-stats", headers=_auth(admin_token)
)
records = admin_client.get(
f"/admin/api/users/{uid}/coin-records",
params={"limit": 10, "cursor": 0},
headers=_auth(admin_token),
)
finally:
event.remove(engine, "before_cursor_execute", reject_full_ad_reward_projection)
assert stats.status_code == 200, stats.text
assert records.status_code == 200, records.text
def test_user_filter_by_status(admin_client: TestClient, admin_token: str) -> None: def test_user_filter_by_status(admin_client: TestClient, admin_token: str) -> None:
_seed_user_with_data("13800000003") _seed_user_with_data("13800000003")
r = admin_client.get("/admin/api/users", params={"status": "active"}, headers=_auth(admin_token)) r = admin_client.get("/admin/api/users", params={"status": "active"}, headers=_auth(admin_token))