## 背景(用户反馈) 「精英宝典和电竞手册已开通的话,应该增加时间展示,可以看到开通时间和过期时间」 - 精英宝典:开通时间来自事件;过期时间 `elite_expire_time` 本来就有 - 电竞手册:开通时间来自事件;**过期时间原来没有落库** —— 只在 `_refresh_esports_handbook` 的返回值里带着,界面上只能看到「已开通」 ## 改动 - 新增 `accounts.esports_expire_time`(迁移 0049,与 `elite_expire_time` 对称), 刷新电竞手册时写回 - 详情页:**已开通才显示**「开通 x / 到期 x」两行小字;顺手删掉底部那段 重复的「开通时间」说明(已挪到每项旁边) - 新增 `formatUnixSeconds()`:斗鱼的到期时间是**秒级时间戳**, 原 `formatTime()` 只收字符串;新函数对 `0`/空/负数一律显示 `-` ## 明确回答:历史开通记录**查不到** 实测官方兑换记录里**只有商品兑换、没有开通记录**(账号 61:精英 2 条都是套装)。 我们自己的 `elite_opened` / `esports_opened` 事件也只有上线后的。 所以历史上的开通时间拿不回来 —— 按用户说的「没有就算了」, 并把这点写进界面说明,别再让人去找。 ## 测的时候抓出一个真 bug(又是 1970) 电竞到期时间的 `"0"`(斗鱼用它表示「没有到期时间」)被原样存成 `0` → 界面会显示 **1970/1/1**。已修(`0`/空 → `None`)并加回归用例 —— 这个坑在积分到期上踩过一次,这次差点重演。 ## 迁移 0048 改名 `20261004_0048_overview_summary.py` → `20261004_0048_high_value_owned.py`: 上一轮把内容从「汇总」收敛成单一用途的 `high_value_owned`,文件名没跟着改。 revision id 不变、迁移状态不受影响;同时清了 `__pycache__` (本仓库被过期 `.pyc` 咬过,症状是「改了没生效」)。 ## 验证(本地) `TZ=Asia/Shanghai` **608 passed / 6 skipped**;ruff 全绿;前端 build 通过、lint 0 errors。
114 lines
4.3 KiB
Python
114 lines
4.3 KiB
Python
"""账号列表要显示一键查询的结果:加一个**极小**的字段
|
||
|
||
账号列表的「兑换记录」列一直来自我们自己的 `account_events`,
|
||
而事件是**上线后才开始记**的 —— 更早的兑换在列表里就是空的,
|
||
看起来像"从没兑换过"(用户报的正是这个:账号 6966677706 明明有官方兑换记录,
|
||
列表却显示未兑换)。
|
||
|
||
一键查询能查到官方兑换记录,但那份 JSON 有 1~2KB。
|
||
账号列表是全站最热的接口之一,15000 个账号各带一份会把响应放大到几十 MB,
|
||
所以**不能**直接把 `overview_snapshot` 返回给列表。
|
||
|
||
这里加一个只含列表所需两项的小字段:
|
||
`{"owned": ["兔兔白日梦"], "exchanges": 2}`
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
|
||
import sqlalchemy as sa
|
||
from alembic import op
|
||
|
||
revision: str = "20261004_0048"
|
||
down_revision: str | None = "20261004_0047"
|
||
branch_labels: str | None = None
|
||
depends_on: str | None = None
|
||
|
||
|
||
def _backfill_from_snapshot() -> None:
|
||
"""从已有的 `overview_snapshot` 回填。
|
||
|
||
必须回填:加这个字段之前已经查过的账号,快照里**早就有**这两项信息,
|
||
不回填的话列表会对它们显示「未查询」—— 明明查过,界面却在骗人。
|
||
"""
|
||
conn = op.get_bind()
|
||
rows = conn.execute(
|
||
sa.text(
|
||
"SELECT id, overview_snapshot FROM accounts "
|
||
"WHERE overview_snapshot IS NOT NULL"
|
||
)
|
||
).fetchall()
|
||
updated = 0
|
||
for account_id, snapshot in rows:
|
||
data = snapshot
|
||
if isinstance(data, (str, bytes)):
|
||
try:
|
||
data = json.loads(data)
|
||
except ValueError:
|
||
continue
|
||
if not isinstance(data, dict):
|
||
continue
|
||
goods = data.get("high_value_goods")
|
||
owned = [
|
||
g.get("name")
|
||
for g in (goods if isinstance(goods, list) else [])
|
||
if isinstance(g, dict) and g.get("owned") and g.get("name")
|
||
]
|
||
# 只留「已兑换过哪些高价值套装」。
|
||
# 兑换条数不再存这里 —— 列表改成从**官方事件**聚合(口径更准,
|
||
# 而且不用维护两套计数)。
|
||
owned_value = owned
|
||
conn.execute(
|
||
sa.text("UPDATE accounts SET high_value_owned = :owned WHERE id = :id"),
|
||
{"owned": json.dumps(owned_value, ensure_ascii=False), "id": account_id},
|
||
)
|
||
updated += 1
|
||
# ⚠️ **不要**在这里 `conn.commit()`。
|
||
# 曾经加过,后果很隐蔽:DDL(ALTER TABLE)在 MySQL 上是自动提交的,
|
||
# 但 commit 会打乱 alembic 自己的版本记账 —— 表现为
|
||
# **列加上了、版本号却没更新**,下次启动升级时又去 ADD COLUMN,
|
||
# 直接 `Duplicate column name` 把服务起不来(生产上就是启动即挂)。
|
||
# 数据改动用 `op.get_bind().execute(...)` 写即可,提交由 alembic 负责
|
||
# (仓库里 0042 的 S1 回填就是这么写的,一直正常)。
|
||
if updated:
|
||
print(f"[0048] 已从快照回填 {updated} 个账号的 high_value_owned")
|
||
|
||
|
||
def _column_exists(name: str) -> bool:
|
||
"""列是否已存在。
|
||
|
||
应用启动时会自动跑迁移,所以迁移必须**可安全重跑** ——
|
||
万一出现「列已在、版本号没更新」的状态(本迁移就踩过),
|
||
无脑 ADD COLUMN 会直接 `Duplicate column name` 让服务起不来。
|
||
"""
|
||
conn = op.get_bind()
|
||
count = conn.execute(
|
||
sa.text(
|
||
"SELECT COUNT(*) FROM information_schema.columns "
|
||
"WHERE table_schema = DATABASE() "
|
||
"AND table_name = 'accounts' AND column_name = :name"
|
||
),
|
||
{"name": name},
|
||
).scalar()
|
||
return bool(count)
|
||
|
||
|
||
def upgrade() -> None:
|
||
if not _column_exists("high_value_owned"):
|
||
op.add_column(
|
||
"accounts",
|
||
sa.Column(
|
||
"high_value_owned",
|
||
sa.JSON(),
|
||
nullable=True,
|
||
comment="一键查询得到的「已兑换过的高价值套装名」列表(列表用,极小)",
|
||
),
|
||
)
|
||
# 无论列是不是这一步加的,都跑回填(幂等:覆盖式写入)
|
||
_backfill_from_snapshot()
|
||
|
||
|
||
def downgrade() -> None:
|
||
op.drop_column("accounts", "high_value_owned")
|