feat(overview): 关掉和平小店查询 + 修电竞「已开通」不落库
## ① 和平小店总开关(默认关)
小店目前是**关闭状态**,但不跳过的话一键查询仍会对每个账号发小店请求,
结果 100% 失败 —— 实测 200 个账号里 `exchange_records_peace` 失败 **195 次(100%)**:
Livelink登录态校验失败
100% 意味着不是账号问题。这既淹掉真错误、又让人以为出故障。
新增 `douyu_config.peace_enabled`(迁移 `0050`,**默认 False**)。
关着时**完全跳过**小店的所有请求(绑定 + 兑换记录),并且**不记 errors** ——
「配置成关的」和「查失败」是两回事,混在一起会让 errors 失去意义。
将来小店恢复,在后台打开这个开关即可;打开后小店失败仍会**如实暴露**
(有用例专门钉住这一点,别把开关当成遮羞布)。
## ② 修:电竞手册「已开通」不会被一键查询更新
用户问"电竞到期时间为什么拿不到",查下去发现**两层**:
**第一层不是 bug**:那 200 个账号 `esports_opened` 全是 0(没开通电竞手册),
所以 `expireTime="0"`、没有到期时间可存。注意
**"电竞已绑定角色"≠"电竞手册已开通"**。实测 `opened_but_no_expire = 0`,没有异常。
**第二层是真 bug(不对称)**:
_refresh_elite_handbook → 写 account.elite_opened ✅
_refresh_esports_handbook → 不写 account.esports_opened ❌
`esports_opened` 只在"开通"任务里被设置,所以**详情页电竞的「已开通/未开通」
根本不会被一键查询更新**。已改成与精英对称(口径同 create_esports_qr:
`manual_type >= 1` 即已开通)。
## 测试(+2)
- 小店关着时:**不发**小店请求、**不查**小店绑定、**不记** peace 相关的 errors,
且结果里标明 `peace_skipped`
- 小店打开时:失败**仍然如实暴露**(反向用例,防止开关变成遮羞布)
## 验证(本地)
TZ=Asia/Shanghai **620 passed / 6 skipped**;ruff 全绿。
This commit is contained in:
@@ -1339,6 +1339,8 @@ class TestOverviewWritesBindStatus:
|
||||
"bind_act_alias": "B",
|
||||
"esports_act_alias": "S",
|
||||
"xpd_act_alias": "X",
|
||||
# 这个用例要覆盖三个作用域,显式打开小店(默认是关的)
|
||||
"peace_enabled": True,
|
||||
"xpd_rid": "R",
|
||||
"xpd_act_id": "1",
|
||||
},
|
||||
@@ -1461,3 +1463,127 @@ class TestDuplicateEventIsQuiet:
|
||||
other = Exception("x")
|
||||
other.orig = _Orig(1048, "Column 'a' cannot be null") # type: ignore[attr-defined]
|
||||
assert _is_duplicate_entry(other) is False
|
||||
|
||||
|
||||
class TestPeaceScopeCanBeTurnedOff:
|
||||
"""和平小店总开关:关着时**完全跳过**它的请求。
|
||||
|
||||
小店目前是关闭状态,但不跳过的话一键查询仍会对每个账号发小店请求、
|
||||
结果 100% 失败:
|
||||
|
||||
实测 200 个账号里 exchange_records_peace 失败 195 次(100%)
|
||||
Livelink登录态校验失败
|
||||
|
||||
这既淹掉真错误、又让人以为出故障。关着时应当**不查、也不记 errors** ——
|
||||
「配置成关的」和「查失败」是两回事。
|
||||
"""
|
||||
|
||||
def setup_method(self):
|
||||
self.engine = get_test_engine()
|
||||
self.session = make_test_session(self.engine)
|
||||
self.user = User(username="peace-admin", password_hash="h", role="super_admin")
|
||||
self.session.add(self.user)
|
||||
self.session.flush()
|
||||
self.account = Account(
|
||||
username="peace-acc", password="p", email="p@e.com", email_password="ep"
|
||||
)
|
||||
self.session.add(self.account)
|
||||
self.session.commit()
|
||||
|
||||
def teardown_method(self):
|
||||
self.session.close()
|
||||
|
||||
def _run(self, *, peace_enabled: bool):
|
||||
from web.backend.services.douyu_runner_overview import OverviewMixin
|
||||
|
||||
called: list[str] = []
|
||||
marked: dict = {}
|
||||
|
||||
class _H(OverviewMixin):
|
||||
def _client(self, c):
|
||||
return _FakeClient()
|
||||
|
||||
def _mark_task(self, db, task, status, message, result=None):
|
||||
# 捕获结果:这个桩是 no-op,不捕获的话 task.result 永远是空
|
||||
marked.update({"status": status, "result": result or {}})
|
||||
|
||||
def _push_log(self, *a, **k):
|
||||
pass
|
||||
|
||||
def _fetch_bind_info(self, client, scope, alias):
|
||||
called.append(f"bind:{scope}")
|
||||
return {"is_bound_role": True}
|
||||
|
||||
def _fetch_xpd_records(self, c, cfg, mid, emid):
|
||||
called.append("xpd_records")
|
||||
raise RuntimeError("Livelink登录态校验失败")
|
||||
|
||||
def _refresh_account_gold_balance(self, c, a):
|
||||
return {"gold_balance": 1, "exchange_balance": 1}
|
||||
|
||||
def _refresh_account_points(self, c, a, cookie, *, ctn=None):
|
||||
return {"points": 1, "points_query": {"raw": {"data": {}}}}
|
||||
|
||||
def _refresh_elite_handbook(self, c, a, *, manual_id, rid=""):
|
||||
return {
|
||||
"elite_opened": False,
|
||||
"elite_expire_time": 0,
|
||||
"elite_manual_score": 0,
|
||||
}
|
||||
|
||||
def _refresh_esports_handbook(self, c, a, *, manual_id):
|
||||
return {"esports_manual_type": 0, "esports_manual_score": 0}
|
||||
|
||||
def _fetch_elite_records(self, c, cfg, mid, emid):
|
||||
return []
|
||||
|
||||
def _fetch_esports_records(self, c, cfg, mid, emid):
|
||||
return []
|
||||
|
||||
task = DouyuTask(
|
||||
batch_id="peace",
|
||||
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",
|
||||
"bind_act_alias": "B",
|
||||
"esports_act_alias": "S",
|
||||
"xpd_act_alias": "X",
|
||||
"xpd_rid": "R",
|
||||
"xpd_act_id": "1",
|
||||
"peace_enabled": peace_enabled,
|
||||
},
|
||||
)
|
||||
self.session.commit()
|
||||
return marked, called
|
||||
|
||||
def test_peace_requests_are_skipped_when_disabled(self):
|
||||
marked, called = self._run(peace_enabled=False)
|
||||
assert "xpd_records" not in called, "小店关着时不该发小店的请求"
|
||||
assert "bind:peace" not in called, "小店关着时也不该查小店的绑定"
|
||||
result = marked["result"]
|
||||
# 关键:**不记 errors** —— 这是配置成关的,不是失败
|
||||
assert not [k for k in (result.get("errors") or {}) if "peace" in k], (
|
||||
"小店是配置成关的,不该记成错误(那会淹掉真错误)"
|
||||
)
|
||||
assert result.get("peace_skipped")
|
||||
|
||||
def test_peace_still_fails_loudly_when_enabled(self):
|
||||
"""打开开关后,小店的失败要**如实暴露** —— 别把开关当成遮羞布。"""
|
||||
marked, called = self._run(peace_enabled=True)
|
||||
assert "xpd_records" in called
|
||||
assert "exchange_records_peace" in marked["result"].get("errors", {})
|
||||
|
||||
@@ -26,7 +26,7 @@ TESTS_DIR = Path(__file__).resolve().parent
|
||||
PROJECT_ROOT = TESTS_DIR.parent
|
||||
VERSIONS_DIR = PROJECT_ROOT / "web" / "backend" / "migrations" / "versions"
|
||||
|
||||
HEAD_REVISION = "20261004_0049"
|
||||
HEAD_REVISION = "20261004_0050"
|
||||
|
||||
# 迁移新增、但不声明在模型里的复合查询索引。
|
||||
EXTRA_INDEXES = (
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"""和平小店总开关:`douyu_config.peace_enabled`(默认关)
|
||||
|
||||
小店目前是**关闭状态**,但一键查询仍会对每个账号发小店的请求,结果 100% 失败:
|
||||
|
||||
Livelink登录态校验失败
|
||||
|
||||
实测 200 个账号里 `exchange_records_peace` 失败 195 次(100%)。这既淹掉真错误,
|
||||
又让人以为出了故障。加一个总开关,关着时**完全跳过**小店的所有请求
|
||||
(不是"发出去再记个错")。
|
||||
|
||||
默认 False —— 与"小店当前关闭"的现状一致;将来恢复时在后台打开即可。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20261004_0050"
|
||||
down_revision: str | None = "20261004_0049"
|
||||
branch_labels: str | None = None
|
||||
depends_on: str | None = None
|
||||
|
||||
COLUMN = "peace_enabled"
|
||||
|
||||
|
||||
def _column_exists(name: str) -> bool:
|
||||
"""可安全重跑(应用启动时会自动跑迁移)。"""
|
||||
conn = op.get_bind()
|
||||
count = conn.execute(
|
||||
sa.text(
|
||||
"SELECT COUNT(*) FROM information_schema.columns "
|
||||
"WHERE table_schema = DATABASE() "
|
||||
"AND table_name = 'douyu_config' AND column_name = :name"
|
||||
),
|
||||
{"name": name},
|
||||
).scalar()
|
||||
return bool(count)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
if not _column_exists(COLUMN):
|
||||
op.add_column(
|
||||
"douyu_config",
|
||||
sa.Column(
|
||||
COLUMN,
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default=sa.text("0"),
|
||||
comment="和平小店总开关;关着时一键查询完全跳过小店请求",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("douyu_config", COLUMN)
|
||||
@@ -445,6 +445,9 @@ class DouyuConfig(Base):
|
||||
esports_chicken_skin_id: Mapped[str] = mapped_column(String(64), default="0")
|
||||
esports_firework_gift_id: Mapped[str] = mapped_column(String(64), default="24767")
|
||||
esports_firework_skin_id: Mapped[str] = mapped_column(String(64), default="3850")
|
||||
# 和平小店总开关。**默认关** —— 小店目前是关闭状态,关着时一键查询
|
||||
# 完全跳过它的所有请求(不再产生 100% 的 Livelink 报错噪音)。
|
||||
peace_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
xpd_act_alias: Mapped[str] = mapped_column(String(64), default="20260623KDQFH")
|
||||
xpd_act_id: Mapped[str] = mapped_column(String(64), default="46195")
|
||||
xpd_rid: Mapped[str] = mapped_column(String(64), default="9263298")
|
||||
|
||||
@@ -203,6 +203,12 @@ class ManualMixin:
|
||||
manual_type = self._to_int(result.get("manual_type"))
|
||||
manual_score = self._to_int(result.get("manual_score"))
|
||||
account.esports_points = manual_score
|
||||
# 开通状态也要落库 —— 与精英对称。
|
||||
# 之前只有 elite 那支写 `account.elite_opened`,电竞的 `esports_opened`
|
||||
# **只在"开通"任务里被设置**,于是详情页的电竞「已开通/未开通」
|
||||
# 根本不会被一键查询更新(用户报"电竞到期时间拿不到"时顺带查出来的)。
|
||||
# 判定口径与 create_esports_qr 一致:manual_type >= 1 即已开通。
|
||||
account.esports_opened = manual_type is not None and manual_type >= 1
|
||||
# 到期时间也落库 —— 否则界面只能显示"已开通",看不到什么时候到期
|
||||
expire_raw = result.get("expire_time")
|
||||
expire_int = (
|
||||
|
||||
@@ -439,12 +439,25 @@ class OverviewMixin:
|
||||
# `bind_status` 能推的推、推不出的留 `unknown`,于是界面上 97% 显示"未知"。
|
||||
# 实测校准:开发库 4 个账号里 3 个其实**已绑定**(`is_bound_role=True` 且有角色名),
|
||||
# 全被 unknown 盖住了。
|
||||
# 和平小店总开关:关着时**完全跳过**它的所有请求。
|
||||
# 小店目前是关闭状态,不跳过的话每个账号都会产生一条
|
||||
# "Livelink登录态校验失败"(实测 200 个里失败 195 次,100%),
|
||||
# 既淹掉真错误、又让人以为出故障。跳过时**不记 errors** ——
|
||||
# 这是配置成关的,不是失败。
|
||||
peace_on = bool(config.get("peace_enabled"))
|
||||
if not peace_on:
|
||||
overview["peace_skipped"] = (
|
||||
"和平小店已关闭(douyu_config.peace_enabled=False)"
|
||||
)
|
||||
|
||||
overview["bind"] = {}
|
||||
for scope, label, alias_key in (
|
||||
("elite", "精英宝典", "bind_act_alias"),
|
||||
("esports", "电竞手册", "esports_act_alias"),
|
||||
("peace", "和平小店", "xpd_act_alias"),
|
||||
):
|
||||
if scope == "peace" and not peace_on:
|
||||
continue
|
||||
alias = str(config.get(alias_key) or "").strip()
|
||||
if not alias:
|
||||
errors[f"bind_{scope}"] = f"未配置{label}活动别名"
|
||||
@@ -480,6 +493,8 @@ class OverviewMixin:
|
||||
("esports", "电竞手册", self._fetch_esports_records),
|
||||
("peace", "和平小店", self._fetch_xpd_records),
|
||||
):
|
||||
if scope == "peace" and not peace_on:
|
||||
continue
|
||||
key = f"exchange_records_{scope}"
|
||||
if not self._scope_configured(scope, manual_id, esports_manual_id, config):
|
||||
errors[key] = f"未配置{label}活动参数,跳过"
|
||||
|
||||
@@ -129,6 +129,8 @@ DOUYU_CONFIG_DEFAULTS = {
|
||||
"gold_api_account_template_name": "斗鱼昵称",
|
||||
"gift_id": "23643",
|
||||
"skin_id": "2942",
|
||||
# 和平小店总开关,默认关(小店当前关闭;关着时一键查询完全跳过它的请求)
|
||||
"peace_enabled": False,
|
||||
"xpd_act_alias": "20260623KDQFH",
|
||||
"xpd_act_id": "46195",
|
||||
"xpd_rid": "9263298",
|
||||
|
||||
Reference in New Issue
Block a user