修复提现审核详情点击用户后提示操作失败 (#171)
## 问题现象
在“提现审核”页点击用户所在行后,提现单主详情可以打开,但用户统计和金币记录区域为空,页面连续提示“操作失败”。
## 原因说明
打开抽屉时前端会继续请求两个子接口:
- `/admin/api/users/{user_id}/reward-stats`
- `/admin/api/users/{user_id}/coin-records`
这两个接口原来都使用 `select(AdRewardRecord)` 加载完整 ORM 对象。SQLAlchemy 会把模型映射的所有列自动展开到 SQL 中,其中包括后来新增的 `boost_round_id`。当旧本地数据库或滚动发布中的数据库尚未补齐该列时,即使提现详情本身完全不使用这个字段,查询仍会报 `no such column: ad_reward_record.boost_round_id`,两个接口均返回 500。
前端的统一错误处理只会展示响应 JSON 中字符串类型的 `detail`;该 500 返回的是普通 `Internal Server Error`,因此最终回退成通用文案“操作失败”。本地前端开启了 React Strict Mode,初始化副作用在开发环境会执行两次,所以两个失败接口会形成截图中的四条“操作失败”提示。
## 修复方案
- 用户奖励统计只查询实际需要的 `ecpm_raw`、`coin` 等字段。
- 金币记录只查询页面展示、排序所需字段。
- 同步缩小信息流广告和签到记录的字段投影,避免将来新增无关 ORM 列再次拖垮详情页。
- 增加 SQL 级回归测试:主动拦截任何包含 `ad_reward_record.boost_round_id` 的详情查询,并验证两个接口仍返回 200。
该改动不会改变统计口径或返回结构。数据库迁移仍应正常执行;这里增加的是旧库及滚动发布期间的向后兼容保护。
## 验证结果
- `pytest tests/test_admin_read.py -q`:17 passed
- 新增回归测试覆盖 `reward-stats` 与 `coin-records`
- `git diff --check`:通过
- 本地实际提现用户接口验证:两个接口均返回 200
---------
Co-authored-by: unknown <798648091@qq.com>
Reviewed-on: #171
Co-authored-by: linkeyu <linkeyu@wonderable.ai>
Co-committed-by: linkeyu <linkeyu@wonderable.ai>
This commit was merged in pull request #171.
This commit is contained in:
@@ -1171,24 +1171,30 @@ def user_reward_stats(
|
||||
acc = db.get(CoinAccount, user_id) # 现金余额:当前快照,不随窗口
|
||||
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.reward_scene == "reward_video",
|
||||
AdRewardRecord.status == "granted",
|
||||
*_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_coins = sum(r.coin for r in rv)
|
||||
|
||||
feed = list(db.execute(
|
||||
select(AdFeedRewardRecord).where(
|
||||
feed = db.execute(
|
||||
select(
|
||||
AdFeedRewardRecord.unit_count,
|
||||
AdFeedRewardRecord.ecpm_raw,
|
||||
AdFeedRewardRecord.coin,
|
||||
).where(
|
||||
AdFeedRewardRecord.user_id == user_id,
|
||||
AdFeedRewardRecord.status == "granted",
|
||||
*_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_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_to = date_to + timedelta(hours=8) if date_to is not None else None
|
||||
|
||||
# 三类来源都只取页面需要的列,避免无关 ORM 新列造成旧库查询失败。
|
||||
for rec in db.execute(
|
||||
select(AdRewardRecord)
|
||||
select(
|
||||
AdRewardRecord.reward_scene,
|
||||
AdRewardRecord.created_at,
|
||||
AdRewardRecord.ecpm_raw,
|
||||
AdRewardRecord.coin,
|
||||
)
|
||||
.where(
|
||||
AdRewardRecord.user_id == user_id,
|
||||
AdRewardRecord.status == "granted",
|
||||
@@ -1255,7 +1267,7 @@ def user_coin_records(
|
||||
)
|
||||
.order_by(AdRewardRecord.created_at.desc())
|
||||
.limit(fetch)
|
||||
).scalars():
|
||||
).all():
|
||||
is_video = rec.reward_scene == "reward_video"
|
||||
rows.append({
|
||||
"source": rec.reward_scene,
|
||||
@@ -1266,7 +1278,12 @@ def user_coin_records(
|
||||
})
|
||||
|
||||
for rec in db.execute(
|
||||
select(AdFeedRewardRecord)
|
||||
select(
|
||||
AdFeedRewardRecord.feed_scene,
|
||||
AdFeedRewardRecord.created_at,
|
||||
AdFeedRewardRecord.ecpm_raw,
|
||||
AdFeedRewardRecord.coin,
|
||||
)
|
||||
.where(
|
||||
AdFeedRewardRecord.user_id == user_id,
|
||||
AdFeedRewardRecord.status == "granted",
|
||||
@@ -1274,7 +1291,7 @@ def user_coin_records(
|
||||
)
|
||||
.order_by(AdFeedRewardRecord.created_at.desc())
|
||||
.limit(fetch)
|
||||
).scalars():
|
||||
).all():
|
||||
rows.append({
|
||||
"source": "feed",
|
||||
"source_label": _FEED_SCENE_LABEL.get(rec.feed_scene, "信息流广告"),
|
||||
@@ -1284,7 +1301,7 @@ def user_coin_records(
|
||||
})
|
||||
|
||||
for rec in db.execute(
|
||||
select(CoinTransaction)
|
||||
select(CoinTransaction.created_at, CoinTransaction.amount)
|
||||
.where(
|
||||
CoinTransaction.user_id == user_id,
|
||||
CoinTransaction.biz_type == "signin",
|
||||
@@ -1292,7 +1309,7 @@ def user_coin_records(
|
||||
)
|
||||
.order_by(CoinTransaction.created_at.desc())
|
||||
.limit(fetch)
|
||||
).scalars():
|
||||
).all():
|
||||
rows.append({
|
||||
"source": "signin",
|
||||
"source_label": "签到",
|
||||
|
||||
Reference in New Issue
Block a user