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:
yml2213
2026-10-04 20:59:17 +08:00
parent 631e33a4f6
commit ccd05cba9c
7 changed files with 209 additions and 1 deletions
@@ -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)