refactor(account): 一键查询不该重复展示账号字段 —— 改成刷新它们,只留账号表里没有的
用户反馈:「一键查询也太重复了,直接更新账号信息不行吗?为啥很多都要再展示一次?
那建立账号信息表有啥用」——**说得对,是我设计错了**。
## 问题
面板把鱼翅 / 积分 / 钱包余额 / 精英宝典 / 电竞手册又原样列了一遍,
而这些账号表(`accounts`)本来就有对应列,账号详情也在显示同一批值 ——
同一屏出现两份,既冗余又让人分不清哪份是准的。
## 改法:查询 = 刷新,不是另开副本
| 内容 | 处理 |
| --- | --- |
| 鱼翅 / 积分 / 交易余额 / 开通状态 | 写回**账号表**(`_refresh_*` 本来就在写),由账号详情已有界面展示 —— **面板不再重复** |
| **积分到期时间** | 有 `accounts.points_expire_at` 列,但**查询漏了没写、界面也没显示** → 现在写回,并加到账号信息区展示 |
| **高价值套装是否已获得** | 账号表没有 → **只有这个该单独展示** |
| **官方兑换记录** | 账号表没有 → 单独展示 |
面板因此瘦身成只含「🎯 高价值套装」+「兑换记录」(+ 错误提示 + 查询时间),
标题也改成「高价值套装与兑换记录(一键查询)」,不再叫"账号信息"。
## 修了一个连带 bug
积分到期时间一直没被查询写回账号(只在快照里躺着),界面也没展示 ——
现在 `_parse_expire_at` 后写入 `accounts.points_expire_at`,
且斗鱼返回 `0` 时写 `None` 而不是 `0`(否则又显示 1970/1/1)。
## 测试(+3,共 41)
`TestOverviewRefreshesAccountFields`:资产/开通状态确实写回账号列、
**积分到期时间写回**、`willExpireTime="0"` 时清成 None 而非 0。
## 验证
`TZ=Asia/Shanghai` **593 passed / 6 skipped**;ruff 全绿;前端 build 通过、lint 0 errors。
This commit is contained in:
@@ -575,3 +575,109 @@ class TestSnapshotExposedOnDetailOnly:
|
||||
assert "overview_snapshot" not in item, (
|
||||
"列表接口不该返回完整快照(每个账号一份 JSON,体积爆炸)"
|
||||
)
|
||||
|
||||
|
||||
class TestOverviewRefreshesAccountFields:
|
||||
"""查询的职责是**刷新账号字段**,不是另开一份副本。
|
||||
|
||||
用户反馈很直接:「一键查询这也太重复了,直接更新账号信息不行吗?
|
||||
那建立账号信息表有啥用」。
|
||||
|
||||
对 —— 鱼翅 / 积分 / 交易余额 / 开通状态账号表本来就有对应列,
|
||||
查询把新值写进去、由已有界面展示即可。这里钉住写回行为,
|
||||
尤其是**积分到期时间**:它有 `accounts.points_expire_at` 列,
|
||||
但之前忘了写(也没在界面上显示)。
|
||||
"""
|
||||
|
||||
def setup_method(self):
|
||||
self.engine = get_test_engine()
|
||||
self.session = make_test_session(self.engine)
|
||||
self.user = User(username="rf-admin", password_hash="h", role="super_admin")
|
||||
self.session.add(self.user)
|
||||
self.session.flush()
|
||||
self.account = Account(
|
||||
username="rf-acc", password="p", email="r@e.com", email_password="ep"
|
||||
)
|
||||
self.session.add(self.account)
|
||||
self.session.commit()
|
||||
|
||||
def teardown_method(self):
|
||||
self.session.close()
|
||||
|
||||
def _run(self, *, expire_at: str = "1795967999"):
|
||||
from web.backend.services.douyu_runner_overview import OverviewMixin
|
||||
|
||||
class _H(OverviewMixin):
|
||||
def _client(self, cookie):
|
||||
return _FakeClient()
|
||||
|
||||
def _mark_task(self, *a, **k):
|
||||
pass
|
||||
|
||||
def _push_log(self, *a, **k):
|
||||
pass
|
||||
|
||||
def _refresh_account_gold_balance(self, c, a):
|
||||
a.gold_balance = 88
|
||||
a.exchange_balance = 9
|
||||
return {"gold_balance": 88, "exchange_balance": 9}
|
||||
|
||||
def _refresh_account_points(self, c, a, cookie, *, ctn=None):
|
||||
a.points = 777
|
||||
return {
|
||||
"points": 777,
|
||||
"points_query": {"raw": {"data": {"willExpireTime": expire_at}}},
|
||||
}
|
||||
|
||||
def _refresh_elite_handbook(self, c, a, *, manual_id, rid=""):
|
||||
a.elite_opened = True
|
||||
return {
|
||||
"elite_opened": True,
|
||||
"elite_expire_time": 0,
|
||||
"elite_manual_score": 5,
|
||||
}
|
||||
|
||||
def _refresh_esports_handbook(self, c, a, *, manual_id):
|
||||
a.esports_opened = True
|
||||
return {"esports_manual_type": 1, "esports_manual_score": 3}
|
||||
|
||||
task = DouyuTask(
|
||||
batch_id="rf",
|
||||
account_id=self.account.id,
|
||||
task_type="query_account_overview",
|
||||
handbook_scope="elite",
|
||||
status="running",
|
||||
message="",
|
||||
created_by=self.user.id,
|
||||
)
|
||||
self.session.add(task)
|
||||
self.session.commit()
|
||||
_H()._execute_query_account_overview(
|
||||
self.session,
|
||||
task,
|
||||
self.account,
|
||||
"c",
|
||||
{"manual_id": "M", "esports_manual_id": "E", "rid": "R"},
|
||||
)
|
||||
self.session.commit()
|
||||
self.session.refresh(self.account)
|
||||
|
||||
def test_assets_are_written_to_account_columns(self):
|
||||
self._run()
|
||||
assert self.account.points == 777
|
||||
assert self.account.gold_balance == 88
|
||||
assert self.account.exchange_balance == 9
|
||||
assert self.account.elite_opened is True
|
||||
assert self.account.esports_opened is True
|
||||
|
||||
def test_points_expire_at_is_written(self):
|
||||
"""积分到期时间有专门的列,查询必须写它(之前漏了)。"""
|
||||
self._run(expire_at="1795967999")
|
||||
assert self.account.points_expire_at is not None
|
||||
assert self.account.points_expire_at.year != 1970
|
||||
|
||||
def test_no_expiry_clears_the_column_not_zero(self):
|
||||
"""斗鱼用 0 表示没有到期时间 —— 不能存成 0(会显示 1970/1/1)。"""
|
||||
self._run(expire_at="0")
|
||||
assert self.account.points_expire_at is None
|
||||
assert self.account.overview_snapshot["assets"]["points_expire_at"] is None
|
||||
|
||||
@@ -999,6 +999,8 @@ def account_overview(
|
||||
"game_channel": acc.game_channel,
|
||||
**bind_payload(acc),
|
||||
# 最近一次「一键查询」快照(列表接口刻意**不**返回,避免体积爆炸)
|
||||
# 积分到期时间:账号自己的字段,账号详情里展示(一键查询会刷新它)
|
||||
"points_expire_at": acc.points_expire_at,
|
||||
"overview_snapshot": acc.overview_snapshot,
|
||||
"overview_queried_at": acc.overview_queried_at,
|
||||
"elite_opened": acc.elite_opened,
|
||||
|
||||
@@ -33,7 +33,7 @@ from sqlalchemy.orm import Session
|
||||
from core.douyu import DouyuActivityClient
|
||||
|
||||
from ..models import Account, DouyuTask
|
||||
from .douyu_runner_donate import _points_expire_at
|
||||
from .douyu_runner_donate import _parse_expire_at, _points_expire_at
|
||||
|
||||
#: 默认关注的高价值商品。按**商品名包含**匹配 —— 官方列表里的名字带前缀
|
||||
#: (实际是「套装-兔兔白日梦」),所以配置里写核心词即可。
|
||||
@@ -246,7 +246,12 @@ class OverviewMixin:
|
||||
overview["assets"]["points"] = points.get("points")
|
||||
# `_refresh_account_points` 只回 {"points", "points_query"},
|
||||
# 到期时间得从 points_query 里取 —— 复用已有具名函数,不自己扒结构。
|
||||
overview["assets"]["points_expire_at"] = _ts(_points_expire_at(points))
|
||||
expire_raw = _ts(_points_expire_at(points))
|
||||
overview["assets"]["points_expire_at"] = expire_raw
|
||||
# **也写回账号**:积分到期时间是账号的信息,本来就该由账号表承载并
|
||||
# 在账号详情里展示 —— 查询只是一次「刷新」,不是另开一份副本。
|
||||
expire_dt = _parse_expire_at(expire_raw)
|
||||
account.points_expire_at = expire_dt
|
||||
except Exception as exc: # noqa: BLE001
|
||||
errors["points"] = _err(exc)
|
||||
|
||||
|
||||
@@ -155,6 +155,8 @@ export interface AccountOverview {
|
||||
points: number | null;
|
||||
gold_balance: number | null;
|
||||
exchange_balance: number | null;
|
||||
/** 积分到期时间(一键查询会刷新它) */
|
||||
points_expire_at: string | null;
|
||||
game_name: string;
|
||||
game_channel: string;
|
||||
/** 三列各管一个作用域:unknown / unbound / pending / bound */
|
||||
|
||||
@@ -380,7 +380,7 @@ export default function AccountDetailPage() {
|
||||
title={
|
||||
<Space>
|
||||
<ThunderboltOutlined />
|
||||
<span>一键查询(斗鱼实时)</span>
|
||||
<span>高价值套装与兑换记录(一键查询)</span>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
@@ -406,8 +406,9 @@ export default function AccountDetailPage() {
|
||||
/>
|
||||
) : (
|
||||
<Text type="secondary">
|
||||
实时向斗鱼查询:鱼翅 / 积分(含到期时间)/ 精英宝典 / 电竞手册 / 官方兑换记录,
|
||||
并重点标出「兔兔白日梦」「狂战萌兔」是否已获得。结果会**存在这个账号上**。
|
||||
实时向斗鱼查一次这个账号:鱼翅 / 积分 / 开通状态会**就地刷新到下面的账号信息**,
|
||||
这里只单独展示账号表里没有的两项 —— 「兔兔白日梦」「狂战萌兔」是否已获得,
|
||||
以及官方兑换记录。
|
||||
</Text>
|
||||
)}
|
||||
</Card>
|
||||
@@ -574,6 +575,12 @@ export default function AccountDetailPage() {
|
||||
|
||||
<div style={{ display: 'flex', gap: 32, marginTop: 16, flexWrap: 'wrap' }}>
|
||||
<Statistic title="当前积分" value={overview?.points ?? '-'} />
|
||||
<Statistic
|
||||
title="积分到期"
|
||||
// 斗鱼用 0 表示「没有到期时间」;不处理会显示成 1970/1/1
|
||||
value={overview?.points_expire_at ? formatTime(overview.points_expire_at) : '-'}
|
||||
valueStyle={{ fontSize: 20 }}
|
||||
/>
|
||||
<Statistic title="鱼翅余额" value={overview?.gold_balance ?? '-'} />
|
||||
<Statistic title="兑换余额" value={overview?.exchange_balance ?? '-'} />
|
||||
<Statistic
|
||||
|
||||
@@ -82,11 +82,6 @@ function formatTime(value: string | number | null | undefined): string {
|
||||
return Number.isNaN(d.getTime()) ? text : d.toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
function yesNo(value: boolean | null | undefined): string {
|
||||
if (value === true) return '已开通';
|
||||
if (value === false) return '未开通';
|
||||
return '未知';
|
||||
}
|
||||
|
||||
/**
|
||||
* 高价值套装卡片。
|
||||
@@ -145,6 +140,16 @@ function HighValueCard({ goods }: { goods: HighValueGoods }) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键查询结果面板。
|
||||
*
|
||||
* ⚠️ **只展示账号表里没有的东西**(高价值套装是否已获得、官方兑换记录)。
|
||||
*
|
||||
* 鱼翅 / 积分 / 钱包余额 / 开通状态这些,账号表本来就有对应字段
|
||||
* (`gold_balance` / `points` / `exchange_balance` / `elite_opened` / `esports_opened`),
|
||||
* 查询的作用是**刷新它们**,然后由账号详情里已有的界面直接反映出来 ——
|
||||
* 在这里再复制一份展示是多余的,还会让人分不清哪份才是准的。
|
||||
*/
|
||||
export default function AccountOverviewPanel({
|
||||
result,
|
||||
queriedAt,
|
||||
@@ -154,7 +159,6 @@ export default function AccountOverviewPanel({
|
||||
queriedAt?: string | null;
|
||||
}) {
|
||||
if (!result) return <Empty description="暂无结果" />;
|
||||
const assets = result.assets || {};
|
||||
const errors = result.errors || {};
|
||||
const goods = result.high_value_goods || [];
|
||||
const records = result.exchange_records || [];
|
||||
@@ -195,42 +199,6 @@ export default function AccountOverviewPanel({
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card size="small" title="资产">
|
||||
<Descriptions size="small" column={2}>
|
||||
<Descriptions.Item label="鱼翅">
|
||||
{assets.gold_balance ?? '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="钱包兑换余额">
|
||||
{assets.exchange_balance ?? '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="积分">{assets.points ?? '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="积分到期">
|
||||
{formatTime(assets.points_expire_at)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
<Card size="small" title="开通状态">
|
||||
<Descriptions size="small" column={2}>
|
||||
<Descriptions.Item label="精英宝典">
|
||||
<Tag color={result.elite?.opened ? 'green' : 'default'}>
|
||||
{yesNo(result.elite?.opened)}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="宝典到期">
|
||||
{formatTime(result.elite?.expire_time)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="电竞手册">
|
||||
<Tag color={result.esports?.opened ? 'green' : 'default'}>
|
||||
{yesNo(result.esports?.opened)}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="电竞积分">
|
||||
{result.esports?.points ?? '-'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
<Card size="small" title="🎯 高价值套装">
|
||||
{goods.length === 0 ? (
|
||||
<Text type="secondary">没有配置关注的高价值套装,或本次没取到</Text>
|
||||
|
||||
Reference in New Issue
Block a user