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={