From aea05952af26d7e01b9cd97e85abf81157cf3af3 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 4 Oct 2026 19:22:34 +0800 Subject: [PATCH] =?UTF-8?q?refactor(account):=20=E4=B8=80=E9=94=AE?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E4=B8=8D=E8=AF=A5=E9=87=8D=E5=A4=8D=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=E8=B4=A6=E5=8F=B7=E5=AD=97=E6=AE=B5=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E6=94=B9=E6=88=90=E5=88=B7=E6=96=B0=E5=AE=83?= =?UTF-8?q?=E4=BB=AC=EF=BC=8C=E5=8F=AA=E7=95=99=E8=B4=A6=E5=8F=B7=E8=A1=A8?= =?UTF-8?q?=E9=87=8C=E6=B2=A1=E6=9C=89=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户反馈:「一键查询也太重复了,直接更新账号信息不行吗?为啥很多都要再展示一次? 那建立账号信息表有啥用」——**说得对,是我设计错了**。 ## 问题 面板把鱼翅 / 积分 / 钱包余额 / 精英宝典 / 电竞手册又原样列了一遍, 而这些账号表(`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。 --- tests/test_account_overview.py | 106 ++++++++++++++++++ web/backend/routers/accounts.py | 2 + web/backend/services/douyu_runner_overview.py | 9 +- web/frontend/src/api/types.ts | 2 + web/frontend/src/pages/AccountDetailPage.tsx | 13 ++- .../douyu-tasks/AccountOverviewPanel.tsx | 52 ++------- 6 files changed, 137 insertions(+), 47 deletions(-) diff --git a/tests/test_account_overview.py b/tests/test_account_overview.py index 90131b9..d399efb 100644 --- a/tests/test_account_overview.py +++ b/tests/test_account_overview.py @@ -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 diff --git a/web/backend/routers/accounts.py b/web/backend/routers/accounts.py index dd90412..6074c74 100644 --- a/web/backend/routers/accounts.py +++ b/web/backend/routers/accounts.py @@ -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, diff --git a/web/backend/services/douyu_runner_overview.py b/web/backend/services/douyu_runner_overview.py index 0d3abda..7f06216 100644 --- a/web/backend/services/douyu_runner_overview.py +++ b/web/backend/services/douyu_runner_overview.py @@ -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) diff --git a/web/frontend/src/api/types.ts b/web/frontend/src/api/types.ts index a0efe78..ace8ecb 100644 --- a/web/frontend/src/api/types.ts +++ b/web/frontend/src/api/types.ts @@ -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 */ diff --git a/web/frontend/src/pages/AccountDetailPage.tsx b/web/frontend/src/pages/AccountDetailPage.tsx index 9e819b5..2c9d54f 100644 --- a/web/frontend/src/pages/AccountDetailPage.tsx +++ b/web/frontend/src/pages/AccountDetailPage.tsx @@ -380,7 +380,7 @@ export default function AccountDetailPage() { title={ - 一键查询(斗鱼实时) + 高价值套装与兑换记录(一键查询) } extra={ @@ -406,8 +406,9 @@ export default function AccountDetailPage() { /> ) : ( - 实时向斗鱼查询:鱼翅 / 积分(含到期时间)/ 精英宝典 / 电竞手册 / 官方兑换记录, - 并重点标出「兔兔白日梦」「狂战萌兔」是否已获得。结果会**存在这个账号上**。 + 实时向斗鱼查一次这个账号:鱼翅 / 积分 / 开通状态会**就地刷新到下面的账号信息**, + 这里只单独展示账号表里没有的两项 —— 「兔兔白日梦」「狂战萌兔」是否已获得, + 以及官方兑换记录。 )} @@ -574,6 +575,12 @@ export default function AccountDetailPage() {
+ ; - 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({ /> )} - - - - {assets.gold_balance ?? '-'} - - - {assets.exchange_balance ?? '-'} - - {assets.points ?? '-'} - - {formatTime(assets.points_expire_at)} - - - - - - - - - {yesNo(result.elite?.opened)} - - - - {formatTime(result.elite?.expire_time)} - - - - {yesNo(result.esports?.opened)} - - - - {result.esports?.points ?? '-'} - - - - {goods.length === 0 ? ( 没有配置关注的高价值套装,或本次没取到