fix(douyu): 加固高价值三态迁移与状态口径

修复迁移中断重跑、旧快照回填覆盖、降级将 unknown 误写成可兑换,以及迁移前 CK 已失效账号未作废旧结论的问题。统一 CK error 的展示与筛选口径,补齐旧快照迁移和兑换兜底反向用例,并同步高价值结论时间语义。同时纳入 .gitignore 更新。
This commit is contained in:
yml2213
2026-11-06 13:25:45 +08:00
parent c9fbbdb828
commit 578e259eea
13 changed files with 286 additions and 68 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ __pycache__/
*.pyc
.claude
.vscode
.openidea
# 环境变量(含敏感信息,勿提交)
.env
+13 -5
View File
@@ -90,6 +90,8 @@ CK 本体和健康结果都在账号上:
列表只返回紧凑的三态 `high_value`(`state` + 每套的 `owned`/`available`/`unknown`),
不返回整份快照,避免响应体积膨胀。
`high_value_checked_at` 是“最近一次形成高价值结论”的时钟,结论本身可以为
`unknown`;它不表示每次执行都成功读到了可用的官方 `status`。
一键查询本身是只读聚合,不直接修改生命周期状态;官方兑换记录会以
`source=official_sync` 幂等补入事件流。
@@ -118,7 +120,8 @@ CK 本体和健康结果都在账号上:
两套都降为 `unknown`,等下一次真的读到官方 `storehome` 才写回。
作废发生在**数据层**(CK 检测结论落定处),不是展示层:CK 恢复之后展示层
就不再降级了,只在展示层降级会让陈旧的「可换」重新变绿。
`error`(查不到)不作废 —— 查不到不等于账号出问题了。
`error`(查不到)既不作废数据,也不在展示层降级 —— 查不到不等于账号状态变了;
它继续显示上一次有效结论,并计入「高价值但 CK 异常」筛选。
### 高价值的三类「查不到」必须分开
@@ -127,13 +130,18 @@ CK 本体和健康结果都在账号上:
| 筛选 | 判据 | 含义 |
| --- | --- | --- |
| `unqueried` | `overview_queried_at IS NULL` | 从没跑过一键查询(**旧口径**,保留不变) |
| `high_value_never` | `high_value_checked_at IS NULL` | 从没成功读到官方商城(商城接口失败 / 没配 `manualID`) |
| `high_value_unknown` | `checked_at` 非空,但已过期或状态是 `unknown` | 查过,但当前结论不可信 |
| `high_value_never` | `high_value_checked_at IS NULL` | 从没形成过高价值结论(商城接口失败 / 没配 `manualID`) |
| `high_value_unknown` | `checked_at` 非空,但已过期或状态是 `unknown` | 形成过结论,但当前结论未知或不可信 |
`high_value_never` 必须单独成档:账号可能查过其它资料、只是商城接口对它
稳定失败。它既不满足 `unqueried`(资料查过),也不满足 `high_value_unknown`
`high_value_never` 必须单独成档:账号可能查过其它资料、只是从未形成高价值结论。
它既不满足 `unqueried`(资料查过),也不满足 `high_value_unknown`
(那条要求 `checked_at` 非空)—— 不单独筛的话,这批账号在界面上**无处可寻**。
0065 迁移会把旧 `overview_snapshot` 回填成新三态。旧快照没保存原始
`status` 时状态保守写 `unknown`,但保留旧查询时间作为结论时钟,让这批账号
进入 6 小时慢速重试;迁移前已经 `CK invalid` 的账号也一律回填为 `unknown`。
没有旧快照的账号则保持 `checked_at=NULL`。
## 自动化策略
- CK 巡检按账号选行,不按登录历史选行。
+36 -4
View File
@@ -343,8 +343,8 @@ class TestOverviewAggregation:
def test_high_value_goods_are_flagged(self):
r = self._run().marked["result"]
goods = {g["name"]: g for g in r["high_value_goods"]}
assert goods["套装-兔兔白日梦"]["owned"] is True # 兑换记录里有
assert goods["套装-狂战萌兔"]["owned"] is False # 记录里没有
assert goods["套装-兔兔白日梦"]["owned"] is True # 官方 status=1
assert goods["套装-狂战萌兔"]["owned"] is False # 官方 status=0
assert "兔兔白日梦" in r["high_value_goods"][0]["name"]
def test_single_item_failure_does_not_fail_the_batch(self):
@@ -1920,7 +1920,7 @@ class TestHighValueAccountFilters:
assert "acc_no_mall" not in names
def test_high_value_never_filter(self):
"""「从未读到官方商城状态」这一档必须**单独可筛**。
"""「从未形成高价值结论」这一档必须**单独可筛**。
之前这批账号(商城接口失败 / 没配 manualID)在界面上无处可寻:
既不满足 ``unqueried``(资料查过),也不满足 ``high_value_unknown``
@@ -2222,6 +2222,37 @@ class TestHighValueTriState:
assert payload["state"] == "stale"
assert payload["stale"] is True
def test_ck_error_keeps_cached_conclusion_in_sync_with_filters(self):
"""CK 检测 error 只表示这次没查成,不改变账号结论或筛选口径。"""
from web.backend.services.account_value import (
high_value_unknown_expr,
suit_available_expr,
)
from web.backend.services.high_value_service import high_value_payload
acc = self._account(
high_value_bairimeng_status="available",
high_value_mengtu_status="available",
high_value_checked_at=datetime.now(UTC),
ck_check_status="error",
)
payload = high_value_payload(acc)
assert payload["state"] == "complete"
assert payload["stale"] is False
assert (
self.session.query(Account)
.filter(Account.id == acc.id, suit_available_expr("bairimeng"))
.count()
== 1
)
assert (
self.session.query(Account)
.filter(Account.id == acc.id, high_value_unknown_expr())
.count()
== 0
)
def test_stale_account_is_not_counted_as_available(self):
"""过期的账号不能进「双套均可兑换」筛选。"""
from web.backend.routers.accounts import accounts_summary
@@ -2397,9 +2428,10 @@ class TestHighValueAfterExchange:
self.account,
manual_id="m",
rid="r",
commodity_name="套装-狂兔萌兔",
commodity_name="套装-狂战萌兔",
)
assert self.account.high_value_bairimeng_status == "unknown"
assert self.account.high_value_mengtu_status == "owned"
assert any(level == "warning" for level, _ in runner.logs)
def test_unrelated_goods_name_is_ignored(self):
+138 -14
View File
@@ -1,6 +1,8 @@
"""0065 高价值三态迁移的 upgrade/downgrade 往返验证(真实 MySQL)。"""
import json
import secrets
from datetime import UTC, datetime
from urllib.parse import quote_plus
from alembic import command
@@ -31,9 +33,67 @@ def test_0065_upgrade_downgrade_roundtrip():
cfg = Config("alembic.ini")
cfg.set_main_option("script_location", "web/backend/migrations")
cfg.set_main_option("sqlalchemy.url", mig_url)
# 先在旧版本落真实数据,验证快照回填,而不是只在空库上验 DDL。
command.upgrade(cfg, "20261021_0064")
engine = create_engine(mig_url)
queried_at = datetime(2026, 11, 6, 12, 0, 0, tzinfo=UTC).replace(tzinfo=None)
snapshots = {
"snapshot-status": (
{
"queried_at": "2026-11-06T12:00:00+00:00",
"high_value_goods": [
{"name": "套装-兔兔白日梦", "status": 1},
{"name": "套装-狂战萌兔", "status": 0},
],
},
"valid",
),
"snapshot-no-status": (
{
"queried_at": "2026-11-05T10:00:00+00:00",
"high_value_goods": [
{"name": "套装-兔兔白日梦", "owned": True},
],
},
"valid",
),
"snapshot-invalid": (
{
"queried_at": "2026-11-04T09:00:00+00:00",
"high_value_goods": [
{"name": "套装-兔兔白日梦", "status": 1},
{"name": "套装-狂战萌兔", "status": 0},
],
},
"invalid",
),
"snapshot-missing": (None, None),
}
with engine.begin() as conn:
for username, (snapshot, ck_status) in snapshots.items():
conn.execute(
text(
"INSERT INTO accounts (username, password, email, email_password,"
" cookie, ck_check_status, overview_snapshot, overview_queried_at,"
" high_value_owned)"
" VALUES (:u, 'p', 'e@x.com', 'p', '', :ck, :snapshot, :queried,"
" :owned)"
),
{
"u": username,
"ck": ck_status or "",
"snapshot": json.dumps(snapshot)
if snapshot is not None
else None,
"queried": queried_at if snapshot is not None else None,
"owned": json.dumps([]) if snapshot is not None else None,
},
)
command.upgrade(cfg, "head")
insp = inspect(create_engine(mig_url))
insp = inspect(engine)
cols = {c["name"] for c in insp.get_columns("accounts")}
assert "high_value_bairimeng_status" in cols
assert "high_value_mengtu_status" in cols
@@ -43,21 +103,78 @@ def test_0065_upgrade_downgrade_roundtrip():
i["name"] for i in insp.get_indexes("accounts")
}
# 塞两行数据:owned/owned 与 owned/unknown,验证降级数组干净
engine = create_engine(mig_url)
with engine.connect() as conn:
rows = conn.execute(
text(
"SELECT username, high_value_bairimeng_status,"
" high_value_mengtu_status, high_value_checked_at"
" FROM accounts ORDER BY username"
)
).fetchall()
by_name = {row[0]: row for row in rows}
assert by_name["snapshot-status"][1:3] == ("owned", "available")
assert by_name["snapshot-status"][3] == queried_at
assert by_name["snapshot-no-status"][1:3] == ("unknown", "unknown")
assert by_name["snapshot-no-status"][3] == datetime(
2026, 11, 5, 10, 0, 0, tzinfo=UTC
).replace(tzinfo=None)
assert by_name["snapshot-invalid"][1:3] == ("unknown", "unknown")
assert by_name["snapshot-invalid"][3] == datetime(
2026, 11, 4, 9, 0, 0, tzinfo=UTC
).replace(tzinfo=None)
assert by_name["snapshot-missing"][1:3] == (None, None)
assert by_name["snapshot-missing"][3] is None
# 模拟“回填和索引已完成,但删旧列前进程中断”:版本仍停在 0064,
# 旧列还在。重跑必须能成功,且不能覆盖已经形成的新结论。
with engine.begin() as conn:
for i, (b, m) in enumerate([("owned", "owned"), ("owned", "unknown")]):
conn.execute(
text("ALTER TABLE accounts ADD COLUMN high_value_owned JSON NULL")
)
conn.execute(
text(
"UPDATE accounts SET high_value_bairimeng_status='available',"
" high_value_mengtu_status='unknown', high_value_checked_at=:now"
" WHERE username='snapshot-status'"
),
{
"now": datetime(2026, 11, 6, 13, 0, 0, tzinfo=UTC).replace(
tzinfo=None
)
},
)
conn.execute(text("UPDATE alembic_version SET version_num='20261021_0064'"))
command.upgrade(cfg, "head")
with engine.connect() as conn:
resumed = conn.execute(
text(
"SELECT high_value_bairimeng_status, high_value_mengtu_status"
" FROM accounts WHERE username='snapshot-status'"
)
).one()
assert resumed == ("available", "unknown")
# 降级无法表达 unknown;必须留 NULL,不能生成 [] 让旧版本误判为可兑换。
with engine.begin() as conn:
for index, (b, m) in enumerate(
[
("available", "available"),
("owned", "available"),
("owned", "owned"),
]
):
conn.execute(
text(
"INSERT INTO accounts (username, password, email, email_password,"
" cookie, high_value_bairimeng_status, high_value_mengtu_status)"
" VALUES (:u, 'p', 'e@x.com', 'p', '', :b, :m)"
" cookie, overview_queried_at, high_value_bairimeng_status,"
" high_value_mengtu_status)"
" VALUES (:u, 'p', 'e@x.com', 'p', '', :q, :b, :m)"
),
{"u": f"rt-{i}", "b": b, "m": m},
{"u": f"downgrade-{index}", "q": queried_at, "b": b, "m": m},
)
command.downgrade(cfg, "20261021_0064")
insp = inspect(create_engine(mig_url))
insp = inspect(engine)
cols = {c["name"] for c in insp.get_columns("accounts")}
assert "high_value_owned" in cols
assert "high_value_bairimeng_status" not in cols
@@ -65,19 +182,26 @@ def test_0065_upgrade_downgrade_roundtrip():
i["name"] for i in insp.get_indexes("accounts")
}
with engine.begin() as conn:
with engine.connect() as conn:
rows = conn.execute(
text(
"SELECT username, high_value_owned FROM accounts ORDER BY username"
)
).fetchall()
# JSON 列读出来是字符串;重点是里面**不能有 null**(旧实现会写出 [null,...])
assert rows[0][1] == '["套装-兔兔白日梦", "套装-狂战萌兔"]', rows[0]
assert rows[1][1] == '["套装-兔兔白日梦"]', rows[1]
assert "null" not in rows[0][1] and "null" not in rows[1][1]
old_values = {
name: json.loads(value) if isinstance(value, str) else value
for name, value in rows
}
assert old_values["snapshot-status"] is None
assert old_values["snapshot-no-status"] is None
assert old_values["snapshot-invalid"] is None
assert old_values["snapshot-missing"] is None
assert old_values["downgrade-0"] == []
assert old_values["downgrade-1"] == ["套装-兔兔白日梦"]
assert old_values["downgrade-2"] == ["套装-兔兔白日梦", "套装-狂战萌兔"]
command.upgrade(cfg, "head")
insp = inspect(create_engine(mig_url))
insp = inspect(engine)
cols = {c["name"] for c in insp.get_columns("accounts")}
assert "high_value_owned" not in cols
assert "high_value_checked_at" in cols
@@ -4,6 +4,10 @@
本迁移增加两套独立状态和检查时间,并从旧快照保守回填:
只有快照里明确保留了官方 ``status=0/1`` 才恢复可兑换/已达上限;
更早的快照没有保存原始 status,统一回填为 unknown,等待一次新查询。
``high_value_checked_at`` 表示“最近一次形成高价值结论的时间”,结论本身可以是
unknown。旧快照虽然没保存原始 status,但确实形成过“当前无法判断”的结论,
因此保留旧查询时间,让这些账号按 unknown 的慢速策略重新确认。
"""
from __future__ import annotations
@@ -49,6 +53,26 @@ def _column_exists(name: str) -> bool:
return bool(count)
def _index_exists(name: str) -> bool:
count = (
op.get_bind()
.execute(
sa.text(
"""
SELECT COUNT(*)
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'accounts'
AND index_name = :name
"""
),
{"name": name},
)
.scalar()
)
return bool(count)
def _json_dict(value: Any) -> dict[str, Any]:
if isinstance(value, dict):
return value
@@ -83,14 +107,19 @@ def _as_datetime(value: Any) -> datetime | None:
def _backfill_from_snapshot() -> None:
"""只回填尚未形成新结论的旧账号,保证迁移中断后可安全重跑。"""
conn = op.get_bind()
rows = conn.execute(
sa.text(
"SELECT id, overview_snapshot, overview_queried_at "
"FROM accounts WHERE overview_snapshot IS NOT NULL"
"SELECT id, overview_snapshot, overview_queried_at, ck_check_status "
"FROM accounts "
"WHERE overview_snapshot IS NOT NULL "
"AND high_value_bairimeng_status IS NULL "
"AND high_value_mengtu_status IS NULL "
"AND high_value_checked_at IS NULL"
)
).fetchall()
for account_id, raw_snapshot, overview_queried_at in rows:
for account_id, raw_snapshot, overview_queried_at, ck_status in rows:
snapshot = _json_dict(raw_snapshot)
goods = snapshot.get("high_value_goods")
states = {key: STATUS_UNKNOWN for key in NAMES}
@@ -102,6 +131,11 @@ def _backfill_from_snapshot() -> None:
if core in name:
states[key] = _snapshot_state(item)
break
# 迁移前已经判定 CK 失效的账号,缓存结论同样不可继续信任。
# 否则部署后列表会显示待确认,但筛选/统计仍把它们算作可兑换,
# 直到下一次巡检才由 on_ck_verdict 补上作废。
if str(ck_status or "") == "invalid":
states = {key: STATUS_UNKNOWN for key in NAMES}
checked_at = _as_datetime(snapshot.get("queried_at") or overview_queried_at)
conn.execute(
sa.text(
@@ -123,6 +157,7 @@ def _backfill_from_snapshot() -> None:
def upgrade() -> None:
old_column_exists = _column_exists("high_value_owned")
for name in (
"high_value_bairimeng_status",
"high_value_mengtu_status",
@@ -137,7 +172,7 @@ def upgrade() -> None:
name,
sa.DateTime(),
nullable=True,
comment="最近一次成功读取官方 storehome 准入状态的时间",
comment="最近一次形成高价值结论的时间;结论可以是 unknown",
),
)
else:
@@ -150,13 +185,16 @@ def upgrade() -> None:
comment="owned / available / unknown;只认官方 storehome.status",
),
)
# 回填只处理新字段全空的账号,所以“加完列但回填前中断”和
# “回填成功但后续 DDL 失败”两种 MySQL 非事务迁移都可以直接重跑。
_backfill_from_snapshot()
op.create_index(
"ix_accounts_high_value_checked_at",
"accounts",
["high_value_checked_at"],
)
if _column_exists("high_value_owned"):
if not _index_exists("ix_accounts_high_value_checked_at"):
op.create_index(
"ix_accounts_high_value_checked_at",
"accounts",
["high_value_checked_at"],
)
if old_column_exists:
op.drop_column("accounts", "high_value_owned")
@@ -170,24 +208,36 @@ def downgrade() -> None:
sa.text(
"""
UPDATE accounts
SET high_value_owned = COALESCE(
(
SELECT JSON_ARRAYAGG(t.name)
FROM (
SELECT CASE WHEN high_value_bairimeng_status = 'owned'
THEN '套装-兔兔白日梦' END AS name
UNION ALL
SELECT CASE WHEN high_value_mengtu_status = 'owned'
THEN '套装-狂战萌兔' END AS name
) AS t
WHERE t.name IS NOT NULL
),
JSON_ARRAY()
)
SET high_value_owned = CASE
WHEN high_value_bairimeng_status IS NULL
OR high_value_bairimeng_status NOT IN ('owned', 'available')
OR high_value_mengtu_status IS NULL
OR high_value_mengtu_status NOT IN ('owned', 'available')
THEN NULL
ELSE COALESCE(
(
SELECT JSON_ARRAYAGG(t.name)
FROM (
SELECT CASE WHEN high_value_bairimeng_status = 'owned'
THEN '套装-兔兔白日梦' END AS name
UNION ALL
SELECT CASE WHEN high_value_mengtu_status = 'owned'
THEN '套装-狂战萌兔' END AS name
) AS t
WHERE t.name IS NOT NULL
),
JSON_ARRAY()
)
END
"""
)
)
op.drop_index("ix_accounts_high_value_checked_at", table_name="accounts")
op.drop_column("accounts", "high_value_checked_at")
op.drop_column("accounts", "high_value_mengtu_status")
op.drop_column("accounts", "high_value_bairimeng_status")
if _index_exists("ix_accounts_high_value_checked_at"):
op.drop_index("ix_accounts_high_value_checked_at", table_name="accounts")
for name in (
"high_value_checked_at",
"high_value_mengtu_status",
"high_value_bairimeng_status",
):
if _column_exists(name):
op.drop_column("accounts", name)
+2
View File
@@ -132,6 +132,8 @@ class Account(Base):
high_value_mengtu_status: Mapped[str | None] = mapped_column(
String(16), nullable=True
)
# 最近一次形成高价值结论的时间;结论可以是 owned / available / unknown。
# 这是“结论时钟”,不是“成功获取可用 status 的时钟”。
high_value_checked_at: Mapped[datetime | None] = mapped_column(
UTCDateTime, nullable=True
)
+3 -2
View File
@@ -88,7 +88,8 @@ def high_value_needs_refresh_expr(
和「资料是否过期」是**两件事**:
- ``overview_queried_at`` 说「上次一键查询是什么时候」;
- ``high_value_checked_at`` 说「上次**成功读到官方商品状态**是什么时候」。
- ``high_value_checked_at`` 说「上次形成高价值结论是什么时候」;
结论可以是 ``unknown``。
两者会脱钩 —— 比如一键查询里商品接口单独失败(其它项都拿到了),
资料时间被刷新了,高价值时间却停在原地。再比如 CK 失效那段时间查不动,
@@ -103,7 +104,7 @@ def high_value_needs_refresh_expr(
而「可兑换」随时会变;
- **结论被作废**(状态是 ``unknown``,比如 CK 失效期间被作废):
按 ``unknown_seconds`` 慢速重试;
- **从来没查到过**(``checked_at IS NULL``):**不算**。那种情况要么
- **从来没形成过结论**(``checked_at IS NULL``):**不算**。那种情况要么
账号确实没查过资料(本来就该被选中),要么商品接口对这个账号稳定失败
(没配 manualID、账号没有宝典)—— 后者若每轮都重选,同一批账号会
反复撞墙,挤掉真正有新欠账的账号。它由「资料过期」那条兜着就够了。
@@ -10,9 +10,9 @@
## 为什么「是否已拥有」能直接查官方
`query_limited_goods` 返回的商品项里 `count` / `foreverCount` / `storeStatus`
是**账号维度**的(不是全局库存)—— 已用生产镜像库实测,见
`extract_high_value_goods` 的文档。
`storehome` 商品项里的账号级 `status`(0=可兑换、1=已达上限)才是唯一判据。
`count` / `foreverCount` / `storeStatus` 是全局库存或商店状态,不能拿来判断
当前账号是否已经兑换。
## 诚实边界
@@ -355,7 +355,7 @@ class OverviewMixin:
goods_query_ok = True
except Exception as exc: # noqa: BLE001
errors["limited_goods"] = _err(exc)
# 商品列表带有 status 准入状态,结合兑换记录在后文判定高价值资产
# 商品列表带有账号级 status;兑换记录只作历史展示,不参与准入判定。
pending_goods = goods_items
# ---- 角色绑定状态(三个作用域都查)----
+6 -5
View File
@@ -221,8 +221,8 @@ def invalidate_high_value(account: Account) -> None:
那条陈旧的「可换」会**重新显示成绿色**,运营就会拿着一份失效结论去兑换。
所以作废是**单向**的:写 ``unknown``,等下一次真的读到官方 ``storehome``
再写回真实的 ``available``/``owned``。``checked_at`` 保留(那确实是
「上次成功读到官方状态」的时间),但结论本身已经不算数。
再写回真实的 ``available``/``owned``。``checked_at`` 保留它作为
「上次形成结论」的时钟,但结论本身已经不算数。
"""
for key in HIGH_VALUE_KEYS:
setattr(account, f"high_value_{key}_status", HIGH_VALUE_STATUS_UNKNOWN)
@@ -236,9 +236,10 @@ def high_value_payload(
checked_at = account.high_value_checked_at
if checked_at is not None and checked_at.tzinfo is None:
checked_at = checked_at.replace(tzinfo=UTC)
# CK 失效 = 查不动了。此时缓存里的「可兑换」已经无法确认当前是否还成立,
# 只能当作待确认 —— 否则运营会拿一个过期结论去兑换。
ck_broken = (account.ck_check_status or "") in {"invalid", "error"}
# 只有明确判定 `invalid` 才降级展示。`error` 表示这次检测没查成,
# 不等于账号状态变了;若也降级,列表会显示已过期,但筛选/统计仍把
# 同一账号算作可兑换,造成同一屏两套结论。
ck_broken = (account.ck_check_status or "") == "invalid"
stale = bool(
ck_broken
or (
+1 -1
View File
@@ -72,7 +72,7 @@ function highValueShortTag(label: string, status: HighValueAssetState) {
function CurrentHighValue({ value }: { value: HighValueStatus }) {
if (value.state === 'never') {
return <Text type="secondary">尚未成功获取官方商城准入状态</Text>;
return <Text type="secondary">尚未形成高价值准入结论</Text>;
}
if (value.state === 'stale') {
return (
@@ -55,7 +55,7 @@ function highValueAssetTag(
function renderHighValueStatus(highValue: HighValueStatus | undefined) {
if (!highValue || highValue.state === 'never') {
return (
<Tooltip title="尚未成功读取官方商城准入状态,点工具栏「批量一键查询」">
<Tooltip title="尚未形成高价值结论,点工具栏「批量一键查询」">
<Text type="secondary">未查询</Text>
</Tooltip>
);
@@ -245,11 +245,11 @@ export function FilterPanel({
},
{
key: 'high_value_never',
label: '未读到商城',
label: '未形成结论',
icon: '❔',
count: summary.high_value?.high_value_never,
color: '#8c8c8c',
tooltip: '从未成功读到官方商城准入状态:商城接口失败,或未配置精英宝典 manualID',
tooltip: '从未形成过高价值结论:商城接口失败、未配置精英宝典 manualID,或账号尚未查询',
active:
highValueFilter === 'high_value_never' &&
minPoints === undefined &&
+1 -1
View File
@@ -83,7 +83,7 @@ export const HIGH_VALUE_OPTIONS = [
{ value: 'both_owned', label: '✓ 两套均已拥有' },
{ value: 'high_value_broken', label: '⚠️ 高价值但CK失效' },
{ value: 'high_value_unknown', label: '❓ 高价值待确认/已过期' },
{ value: 'high_value_never', label: '❓ 从未读到官方商城状态' },
{ value: 'high_value_never', label: '❓ 从未形成高价值结论' },
{ value: 'unqueried', label: '❓ 未查过资料' },
];