feat(account): 一键查询补上「角色绑定状态」—— 以前从来没人查过,97% 显示"未知"
## 问题(用户:"这个做了吗?我看好像都是未知") **只做了一半**: - 列的拆分做了(S6):三列存在、写入收敛到 set_bind_status、迁移映射与运行时对齐 - 但**没有任何一处真的去查绑定状态**(grep -c bind_info douyu_runner_overview.py = 0) 那三列的值从来不是查来的,而是 S6 迁移时从旧的垃圾场列 bind_status **能推的推、推不出的留 unknown**。只把明确陈述了绑定事实的旧值映射过来 (bind_confirmed → bound、bind_qr_generated → pending、game_not_bound → unbound), 其余一律 unknown —— 因为旧列被"查限兑商品""送礼物"这类**无关动作**覆盖过, 从无关动作推绑定状态就是造假。当时留 unknown 是对的,但结果是: 真实生产分布(15182 个账号) 精英 unknown 14683 (96.7%) 电竞 14730 (97.0%) 小店 15045 (99.1%) ## 修法:真的去查,用唯一写入者写回 一键查询新增三个绑定查询(接口都已存在,活动别名走后台配置): bind_info / esports_bind_info / xpd_bind_info。 新增 derive_from_bind_info() 把返回推导成 4 态,**并做了实测校准**: 账号 列值(旧) is_bound_role 角色名 推导 51 unknown False (空) unbound 60 unknown True 唉清静的一生 bound 61 unknown True 柿子洋音 bound 62 unknown True FatalistIc bound **4 个里 3 个其实已绑定,全被 unknown 盖住了。** 两个刻意的设计: 1. **推导不出来时返回 None,且不动原值** —— "没查到"和"没绑定"必须分开, 写 unbound 就是编造结论(小店未开通、返回里没有标志时实测就是这种情况) 2. **不产出 pending** —— 它描述"正在绑定/待确认"这种**进行中的动作** (如刚生成过二维码),那是一次查询的副作用,不是查询能观测到的状态 3. need_bind_role **不是**"未绑定"的信号:实测**已绑定**的账号上它也是 1, 拿它当判据会把这 3 个已绑定的判成未绑 端到端实测(账号 51): 查询前: 精英=unknown 电竞=unknown 小店=unknown 查询后: 精英=unbound 电竞=unbound 小店=unknown ← 推不出来,正确保持原值 ## 显示:把"没查过"和"查了推不出"分开 三处都改了(用户要求):全 unknown 时不再显示「精? 电? 店?」, 而是 **未查询**(点一键查询就有了)/ **未能判定**(斗鱼这次没给结论)。 ## 验证(本地) TZ=Asia/Shanghai **615 passed / 6 skipped**;ruff 全绿;前端 build 通过、lint 0 errors。 新增 9 条用例(含"need_bind_role 不是信号"的反向回归)。
This commit is contained in:
@@ -220,6 +220,10 @@ class _Harness(OverviewMixin):
|
||||
return {"esports_manual_type": 1, "esports_manual_score": 10}
|
||||
|
||||
# handler 按作用域抓官方记录(三套接口),测试里给最小实现
|
||||
def _fetch_bind_info(self, client, scope, alias):
|
||||
# 测试里统一给"已绑定"(有角色名),避免依赖具体假客户端
|
||||
return {"is_bound_role": True, "is_bound_act": True, "role_name": "某角色"}
|
||||
|
||||
def _fetch_elite_records(self, client, config, manual_id, esports_manual_id):
|
||||
return client.exchange_records(manual_id=manual_id).get("records") or []
|
||||
|
||||
@@ -271,6 +275,8 @@ class TestOverviewAggregation:
|
||||
"xpd_act_alias": "A",
|
||||
"xpd_rid": "R",
|
||||
"xpd_act_id": "1",
|
||||
"bind_act_alias": "B",
|
||||
"esports_act_alias": "E",
|
||||
},
|
||||
)
|
||||
return h
|
||||
@@ -481,6 +487,13 @@ class TestSnapshotIsWrittenToAccount:
|
||||
def _refresh_esports_handbook(self, c, a, *, manual_id):
|
||||
return {"esports_manual_type": 0, "esports_manual_score": 0}
|
||||
|
||||
def _fetch_bind_info(self, c, scope, alias):
|
||||
return {
|
||||
"is_bound_role": True,
|
||||
"is_bound_act": True,
|
||||
"role_name": "某角色",
|
||||
}
|
||||
|
||||
def _fetch_elite_records(self, c, cfg, mid, emid):
|
||||
return c.exchange_records(manual_id=mid).get("records") or []
|
||||
|
||||
@@ -513,6 +526,8 @@ class TestSnapshotIsWrittenToAccount:
|
||||
"xpd_act_alias": "A",
|
||||
"xpd_rid": "R",
|
||||
"xpd_act_id": "1",
|
||||
"bind_act_alias": "B",
|
||||
"esports_act_alias": "E",
|
||||
},
|
||||
)
|
||||
self.session.commit()
|
||||
@@ -675,6 +690,13 @@ class TestOverviewRefreshesAccountFields:
|
||||
a.esports_opened = True
|
||||
return {"esports_manual_type": 1, "esports_manual_score": 3}
|
||||
|
||||
def _fetch_bind_info(self, c, scope, alias):
|
||||
return {
|
||||
"is_bound_role": True,
|
||||
"is_bound_act": True,
|
||||
"role_name": "某角色",
|
||||
}
|
||||
|
||||
def _fetch_elite_records(self, c, cfg, mid, emid):
|
||||
return c.exchange_records(manual_id=mid).get("records") or []
|
||||
|
||||
@@ -707,6 +729,8 @@ class TestOverviewRefreshesAccountFields:
|
||||
"xpd_act_alias": "A",
|
||||
"xpd_rid": "R",
|
||||
"xpd_act_id": "1",
|
||||
"bind_act_alias": "B",
|
||||
"esports_act_alias": "E",
|
||||
},
|
||||
)
|
||||
self.session.commit()
|
||||
@@ -878,6 +902,8 @@ class TestSummaryFeedsAccountList:
|
||||
"xpd_act_alias": "A",
|
||||
"xpd_rid": "R",
|
||||
"xpd_act_id": "1",
|
||||
"bind_act_alias": "B",
|
||||
"esports_act_alias": "E",
|
||||
},
|
||||
)
|
||||
self.session.commit()
|
||||
@@ -1157,3 +1183,191 @@ class TestEsportsExpireTimePersisted:
|
||||
assert self.account.esports_expire_time is None
|
||||
self._refresh("")
|
||||
assert self.account.esports_expire_time is None
|
||||
|
||||
|
||||
class TestDeriveFromBindInfo:
|
||||
"""从斗鱼 `bind_info` 推导绑定状态。
|
||||
|
||||
**实测校准过**(开发库 4 个账号):`is_bound_role` 为真时账号确实有角色名,
|
||||
而它们的列值全是 `unknown` —— 4 个里 3 个其实已绑定,被 unknown 盖住了。
|
||||
|
||||
⚠️ `need_bind_role` **不是**"未绑定"的信号:实测**已绑定**的账号上它也是 1。
|
||||
用它会把这 3 个已绑定的判成未绑 —— 幸好没拿它当判据。
|
||||
"""
|
||||
|
||||
def test_bound_when_role_flag_true(self):
|
||||
from web.backend.services.account_binding import (
|
||||
BIND_BOUND,
|
||||
derive_from_bind_info,
|
||||
)
|
||||
|
||||
assert (
|
||||
derive_from_bind_info({"is_bound_role": True, "role_name": "x"})
|
||||
== BIND_BOUND
|
||||
)
|
||||
|
||||
def test_bound_when_act_flag_true(self):
|
||||
"""只有 act 标志为真时也算已绑 —— 避免把"已绑但没回角色名"误报成未绑。"""
|
||||
from web.backend.services.account_binding import (
|
||||
BIND_BOUND,
|
||||
derive_from_bind_info,
|
||||
)
|
||||
|
||||
assert derive_from_bind_info({"is_bound_act": True}) == BIND_BOUND
|
||||
|
||||
def test_unbound_when_flags_explicitly_false(self):
|
||||
from web.backend.services.account_binding import (
|
||||
BIND_UNBOUND,
|
||||
derive_from_bind_info,
|
||||
)
|
||||
|
||||
assert derive_from_bind_info({"is_bound_role": False}) == BIND_UNBOUND
|
||||
assert derive_from_bind_info({"is_bound_act": "0"}) == BIND_UNBOUND
|
||||
|
||||
def test_none_when_no_conclusion(self):
|
||||
"""标志缺失 → None(推不出来),**绝不能**当成 unbound。
|
||||
|
||||
这是"没查到"和"没绑定"的分界:写 unbound 就是编造结论。
|
||||
小店未开通时实测就是这种情况(返回里没有这两个标志)。
|
||||
"""
|
||||
from web.backend.services.account_binding import derive_from_bind_info
|
||||
|
||||
assert derive_from_bind_info({}) is None
|
||||
assert derive_from_bind_info({"role_name": "x"}) is None
|
||||
assert derive_from_bind_info(None) is None
|
||||
|
||||
def test_need_bind_role_is_not_a_signal(self):
|
||||
"""回归:`need_bind_role` 在**已绑定**账号上也是 1,不能拿它判未绑。"""
|
||||
from web.backend.services.account_binding import (
|
||||
BIND_BOUND,
|
||||
derive_from_bind_info,
|
||||
)
|
||||
|
||||
info = {"is_bound_role": True, "is_bound_act": True, "need_bind_role": 1}
|
||||
assert derive_from_bind_info(info) == BIND_BOUND
|
||||
|
||||
|
||||
class TestOverviewWritesBindStatus:
|
||||
"""一键查询要把三个作用域的绑定状态**查出来并写回**。
|
||||
|
||||
这三个值以前从来没人查过,只是 S6 迁移时从旧的垃圾场列能推的推、
|
||||
推不出的留 unknown —— 于是界面上 97% 显示"未知",对运营没用。
|
||||
"""
|
||||
|
||||
def setup_method(self):
|
||||
self.engine = get_test_engine()
|
||||
self.session = make_test_session(self.engine)
|
||||
self.user = User(username="bind-admin", password_hash="h", role="super_admin")
|
||||
self.session.add(self.user)
|
||||
self.session.flush()
|
||||
self.account = Account(
|
||||
username="bind-acc",
|
||||
password="p",
|
||||
email="b@e.com",
|
||||
email_password="ep",
|
||||
role_bind_status="unknown",
|
||||
esports_bind_status="unknown",
|
||||
xpd_bind_status="unknown",
|
||||
)
|
||||
self.session.add(self.account)
|
||||
self.session.commit()
|
||||
|
||||
def teardown_method(self):
|
||||
self.session.close()
|
||||
|
||||
def _run(self, *, info_by_scope):
|
||||
from web.backend.services.douyu_runner_overview import OverviewMixin
|
||||
|
||||
class _H(OverviewMixin):
|
||||
def _client(self, c):
|
||||
return _FakeClient()
|
||||
|
||||
def _mark_task(self, *a, **k):
|
||||
pass
|
||||
|
||||
def _push_log(self, *a, **k):
|
||||
pass
|
||||
|
||||
def _fetch_bind_info(self, client, scope, alias):
|
||||
return info_by_scope[scope]
|
||||
|
||||
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 []
|
||||
|
||||
def _fetch_xpd_records(self, c, cfg, mid, emid):
|
||||
return []
|
||||
|
||||
task = DouyuTask(
|
||||
batch_id="bind",
|
||||
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",
|
||||
},
|
||||
)
|
||||
self.session.commit()
|
||||
self.session.refresh(self.account)
|
||||
return task
|
||||
|
||||
def test_writes_derived_status(self):
|
||||
self._run(
|
||||
info_by_scope={
|
||||
"elite": {"is_bound_role": True, "role_name": "有角色"},
|
||||
"esports": {"is_bound_role": False},
|
||||
"peace": {"is_bound_role": False},
|
||||
}
|
||||
)
|
||||
assert self.account.role_bind_status == "bound"
|
||||
assert self.account.esports_bind_status == "unbound"
|
||||
assert self.account.xpd_bind_status == "unbound"
|
||||
|
||||
def test_none_derivation_keeps_original_value(self):
|
||||
"""推不出来时**不动原值** —— 不能把"没查到"写成"没绑定"。"""
|
||||
self._run(
|
||||
info_by_scope={
|
||||
"elite": {"is_bound_role": True},
|
||||
"esports": {}, # 标志缺失 → 推不出来
|
||||
"peace": {},
|
||||
}
|
||||
)
|
||||
assert self.account.role_bind_status == "bound"
|
||||
assert self.account.esports_bind_status == "unknown", "推不出来应保持原值"
|
||||
assert self.account.xpd_bind_status == "unknown"
|
||||
|
||||
@@ -184,3 +184,48 @@ def bind_payload(account: Account) -> dict[str, str]:
|
||||
payload[f"{prefix}_bind_status"] = state
|
||||
payload[f"{prefix}_bind_label"] = BIND_LABELS[state]
|
||||
return payload
|
||||
|
||||
|
||||
def derive_from_bind_info(info: dict | None) -> str | None:
|
||||
"""从斗鱼 `bind_info` 的返回推导绑定状态;**推导不出来时返回 None**。
|
||||
|
||||
返回 None 表示「这次查询没能得出可靠结论」—— 调用方应当**保持原值或写
|
||||
unknown**,绝不要把它当成 `unbound`。"没查到"和"没绑定"必须分开。
|
||||
|
||||
判定依据(字段含义见 `core/douyu/activity_client.py:_parse_bind_info`):
|
||||
|
||||
- `is_bound_role` 为真 → **bound**(已经绑了角色,这是"角色绑定"的正主)
|
||||
- 否则若 `is_bound_act` 为真 → **bound**(活动维度已绑;两个都真才更稳,
|
||||
但只给 act 时也算已绑,避免把"已绑但没回角色名"误报成未绑)
|
||||
- 两个标志都在、且都为假 → **unbound**(明确说了没绑)
|
||||
- 两个标志**都缺失**(接口没给 / 解析失败)→ **None**(推不出来,别猜)
|
||||
|
||||
⚠️ 这里刻意**不产出 pending**:`pending` 描述的是"正在绑定/待确认"这种
|
||||
**进行中的动作**(例如刚生成过绑定二维码),那是一次查询的**副作用**,
|
||||
不是查询能观测到的状态。硬用查询结果去推 pending 就是编造。
|
||||
"""
|
||||
if not isinstance(info, dict):
|
||||
return None
|
||||
|
||||
def flag(key: str) -> bool | None:
|
||||
value = info.get(key)
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
text = str(value).strip().lower()
|
||||
if text in {"1", "true", "yes", "y"}:
|
||||
return True
|
||||
if text in {"0", "false", "no", "n", ""}:
|
||||
return False
|
||||
return None
|
||||
|
||||
bound_role = flag("is_bound_role")
|
||||
bound_act = flag("is_bound_act")
|
||||
if bound_role is True or bound_act is True:
|
||||
return BIND_BOUND
|
||||
if bound_role is False or bound_act is False:
|
||||
# 至少有一个标志明确为假 —— 接口给了结论,可以判"未绑定"
|
||||
return BIND_UNBOUND
|
||||
# 两个标志都缺失:接口没给结论,保持 unknown,别把"没查到"写成"没绑定"
|
||||
return None
|
||||
|
||||
@@ -33,6 +33,7 @@ from sqlalchemy.orm import Session
|
||||
from core.douyu import DouyuActivityClient
|
||||
|
||||
from ..models import Account, DouyuTask
|
||||
from .account_binding import derive_from_bind_info, set_bind_status
|
||||
from .business_events import record_official_exchange
|
||||
from .douyu_runner_donate import _parse_expire_at, _points_expire_at
|
||||
|
||||
@@ -270,6 +271,16 @@ class OverviewMixin:
|
||||
for k in ("xpd_act_alias", "xpd_rid", "xpd_act_id")
|
||||
)
|
||||
|
||||
def _fetch_bind_info(
|
||||
self, client: DouyuActivityClient, scope: str, alias: str
|
||||
) -> dict[str, Any]:
|
||||
"""按作用域查绑定信息(三个接口不同,收敛在一处)。"""
|
||||
if scope == "elite":
|
||||
return client.bind_info(alias)
|
||||
if scope == "esports":
|
||||
return client.esports_bind_info(alias)
|
||||
return client.xpd_bind_info(act_alias=alias)
|
||||
|
||||
def _fetch_elite_records(
|
||||
self,
|
||||
client: DouyuActivityClient,
|
||||
@@ -422,6 +433,36 @@ class OverviewMixin:
|
||||
# 所以这里放在兑换记录**之后**才能判定 —— 见下面的顺序。
|
||||
pending_goods = goods_items
|
||||
|
||||
# ---- 角色绑定状态(三个作用域都查)----
|
||||
#
|
||||
# 这三个值以前**从来没人查过** —— 只是 S6 迁移时从旧的垃圾场列
|
||||
# `bind_status` 能推的推、推不出的留 `unknown`,于是界面上 97% 显示"未知"。
|
||||
# 实测校准:开发库 4 个账号里 3 个其实**已绑定**(`is_bound_role=True` 且有角色名),
|
||||
# 全被 unknown 盖住了。
|
||||
overview["bind"] = {}
|
||||
for scope, label, alias_key in (
|
||||
("elite", "精英宝典", "bind_act_alias"),
|
||||
("esports", "电竞手册", "esports_act_alias"),
|
||||
("peace", "和平小店", "xpd_act_alias"),
|
||||
):
|
||||
alias = str(config.get(alias_key) or "").strip()
|
||||
if not alias:
|
||||
errors[f"bind_{scope}"] = f"未配置{label}活动别名"
|
||||
continue
|
||||
try:
|
||||
info = self._fetch_bind_info(client, scope, alias)
|
||||
derived = derive_from_bind_info(info)
|
||||
overview["bind"][scope] = {
|
||||
"derived": derived,
|
||||
"role_name": str(info.get("role_name") or info.get("role") or ""),
|
||||
}
|
||||
if derived is not None:
|
||||
# 唯一写入者;推不出来(None)时**不动原值** —— 宁可留 unknown,
|
||||
# 也不要把"没查到"写成"没绑定"
|
||||
set_bind_status(account, scope, derived)
|
||||
except Exception as exc: # noqa: BLE001 外部接口边界需要宽泛兜底
|
||||
errors[f"bind_{scope}"] = _err(exc)
|
||||
|
||||
# ---- 官方兑换记录(**三个作用域都拉**)----
|
||||
#
|
||||
# 精英宝典 / 电竞手册 用的是同一个接口,只是 manual_id 不同;
|
||||
|
||||
@@ -418,18 +418,46 @@ export default function AccountDetailPage() {
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="角色绑定">
|
||||
{/* 三个作用域各一列,取值 unknown/unbound/pending/bound */}
|
||||
<Space size={4} wrap>
|
||||
<Tag color={BIND_TONES[overview?.role_bind_status ?? 'unknown']}>
|
||||
精英:{overview?.role_bind_label || '未知'}
|
||||
</Tag>
|
||||
<Tag color={BIND_TONES[overview?.esports_bind_status ?? 'unknown']}>
|
||||
电竞:{overview?.esports_bind_label || '未知'}
|
||||
</Tag>
|
||||
<Tag color={BIND_TONES[overview?.xpd_bind_status ?? 'unknown']}>
|
||||
小店:{overview?.xpd_bind_label || '未知'}
|
||||
</Tag>
|
||||
</Space>
|
||||
{/* 三个作用域各一列,取值 unknown/unbound/pending/bound。
|
||||
全 unknown 时把「没查过」和「查了推不出」分开说 ——
|
||||
混成一个"未知"会让人不知道下一步该干什么。 */}
|
||||
{[
|
||||
overview?.role_bind_status,
|
||||
overview?.esports_bind_status,
|
||||
overview?.xpd_bind_status,
|
||||
].every((s) => !s || s === 'unknown') ? (
|
||||
<Text type="secondary">
|
||||
{overview?.overview_queried_at ? (
|
||||
<>
|
||||
未能判定
|
||||
<br />
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
已查询过,但斗鱼这次没给出绑定结论
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
未查询
|
||||
<br />
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
点上方「一键查询」即可查出来
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
) : (
|
||||
<Space size={4} wrap>
|
||||
<Tag color={BIND_TONES[overview?.role_bind_status ?? 'unknown']}>
|
||||
精英:{overview?.role_bind_label || '未知'}
|
||||
</Tag>
|
||||
<Tag color={BIND_TONES[overview?.esports_bind_status ?? 'unknown']}>
|
||||
电竞:{overview?.esports_bind_label || '未知'}
|
||||
</Tag>
|
||||
<Tag color={BIND_TONES[overview?.xpd_bind_status ?? 'unknown']}>
|
||||
小店:{overview?.xpd_bind_label || '未知'}
|
||||
</Tag>
|
||||
</Space>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label="归属">
|
||||
|
||||
@@ -447,12 +447,29 @@ export default function AccountsPage() {
|
||||
dataIndex: 'role_bind_status',
|
||||
width: 150,
|
||||
render: (_, record: AccountItem) => {
|
||||
// 三个作用域全都没查过时,显示「—」而不是「精? 电? 店?」——
|
||||
// 满屏问号只是噪声,不如一眼看出「这账号还没查过绑定」。
|
||||
const allUnknown = [record.role_bind_status, record.esports_bind_status, record.xpd_bind_status]
|
||||
.every((s) => !s || s === 'unknown');
|
||||
// 三个作用域全都 unknown 时不再显示「精? 电? 店?」(满屏问号只是噪声),
|
||||
// 但要把两种情况**分开**:
|
||||
// 没查过 → 「未查询」(有救,点一键查询就有了)
|
||||
// 查过但推不出 → 「未能判定」(接口没给结论)
|
||||
// 混成一个词会让人不知道下一步该干什么。
|
||||
const allUnknown = [
|
||||
record.role_bind_status,
|
||||
record.esports_bind_status,
|
||||
record.xpd_bind_status,
|
||||
].every((s) => !s || s === 'unknown');
|
||||
if (allUnknown) {
|
||||
return <Tooltip title="还没有查询过绑定"><Text type="secondary">—</Text></Tooltip>;
|
||||
const queried = Boolean(record.overview_queried_at);
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
queried
|
||||
? '已查询过,但斗鱼这次没给出绑定结论(既非已绑也非未绑)'
|
||||
: '还没查询过绑定 —— 点工具栏「批量一键查询」'
|
||||
}
|
||||
>
|
||||
<Text type="secondary">{queried ? '未能判定' : '未查询'}</Text>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Space size={2} wrap>
|
||||
|
||||
Reference in New Issue
Block a user