From 8f231e6edf1838a9d045eadea50556ad2d51570b Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 4 Oct 2026 21:01:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(binding):=20=E8=A7=92=E8=89=B2=E7=BB=91?= =?UTF-8?q?=E5=AE=9A=E5=8F=AA=E7=9C=8B=20is=5Fbound=5Frole=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E3=80=8C=E6=98=AF=E5=90=A6=E5=BC=80=E9=80=9A?= =?UTF-8?q?=E3=80=8D=E4=B8=8E=E3=80=8C=E6=98=AF=E5=90=A6=E7=BB=91=E5=AE=9A?= =?UTF-8?q?=E8=A7=92=E8=89=B2=E3=80=8D=E6=98=AF=E4=B8=A4=E4=BB=B6=E4=BA=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户明确指出:**精英/电竞是否开通,和是否绑定角色没关系**。 ## 我确实混了(虽然这次没触发) `derive_from_bind_info` 里有个兜底: if bound_role is True or bound_act is True: return BOUND `role_bind_status` 这一列问的是「**角色**绑没绑」,而 `is_bound_act` 是 「**活动**维度」的标志 —— 拿它兜底就是把两件事混起来。 一旦有账号 `is_bound_act=True` 而角色未绑,就会报出"角色已绑定"的**错结论**。 ## 生产数据证实了用户的说法 查已查询的 196 个账号: 电竞 opened=False 且 bind=bound → **187 个** 也就是**187 个账号绑了角色、但电竞手册压根没开通** —— 两者完全独立。 同时数据也证明 `is_bound_role` 本身就够用(不需要那个兜底): 判为 bound 的里面:精英 183 个全都有角色名(0 个没有) 电竞 187 个全都有角色名(0 个没有) ## 改法 去掉 `is_bound_act` 的参与,**只看 `is_bound_role`**: is_bound_role 真 → bound is_bound_role 假 → unbound 缺失 → None(保持 unknown,别把"没查到"写成"没绑定") 语义上不再有"把活动当角色"的路径。 ## 测试 把两条**编码了旧行为**的用例改掉,并补一条钉住领域事实: - 只有 `is_bound_act` 没有 `is_bound_role` → **None**(推不出角色结论) - `{"is_bound_role": True, "manual_type": 0, "expire_time": "0"}` → **bound** (绑了角色+没开通,两者独立,不该因为"没开通"而变成别的) ## 验证(本地) TZ=Asia/Shanghai **620 passed / 6 skipped**;ruff 全绿。 --- tests/test_account_overview.py | 33 +++++++++++++++++++++---- web/backend/services/account_binding.py | 30 ++++++++++++++-------- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/tests/test_account_overview.py b/tests/test_account_overview.py index e826908..02f9f74 100644 --- a/tests/test_account_overview.py +++ b/tests/test_account_overview.py @@ -1206,23 +1206,46 @@ class TestDeriveFromBindInfo: == BIND_BOUND ) - def test_bound_when_act_flag_true(self): - """只有 act 标志为真时也算已绑 —— 避免把"已绑但没回角色名"误报成未绑。""" + def test_role_flag_is_the_only_signal(self): + """**只看 is_bound_role** —— 这一列问的是「角色绑没绑」。 + + 曾经用 is_bound_act 做兜底("避免漏掉已绑但没回角色名的"), + 那是把"活动维度"当成"角色维度"。实测那些判为 bound 的账号 + **100% 都有角色名**,证明 is_bound_role 本身就够用。 + """ from web.backend.services.account_binding import ( BIND_BOUND, derive_from_bind_info, ) - assert derive_from_bind_info({"is_bound_act": True}) == BIND_BOUND + assert derive_from_bind_info({"is_bound_role": True}) == BIND_BOUND + # 只有 act 没有 role → **推不出角色结论**,保持 unknown(别拿活动当角色) + assert derive_from_bind_info({"is_bound_act": True}) is None + assert derive_from_bind_info({"is_bound_act": "1"}) is None - def test_unbound_when_flags_explicitly_false(self): + def test_unbound_when_role_flag_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 + assert derive_from_bind_info({"is_bound_role": "0"}) == BIND_UNBOUND + + def test_opened_and_bound_are_independent(self): + """领域事实:**「是否开通」与「是否绑定角色」互不相干**(用户明确指出)。 + + 生产实测印证:**187 个账号「绑了角色」但「电竞手册未开通」**。 + 所以推导只看 is_bound_role,绝不去看"开通"相关的任何标志。 + """ + from web.backend.services.account_binding import ( + BIND_BOUND, + derive_from_bind_info, + ) + + # 绑了角色、没开通 → bound(两者独立,不该因为"没开通"而变成别的) + info = {"is_bound_role": True, "manual_type": 0, "expire_time": "0"} + assert derive_from_bind_info(info) == BIND_BOUND def test_none_when_no_conclusion(self): """标志缺失 → None(推不出来),**绝不能**当成 unbound。 diff --git a/web/backend/services/account_binding.py b/web/backend/services/account_binding.py index 490e09c..22b783d 100644 --- a/web/backend/services/account_binding.py +++ b/web/backend/services/account_binding.py @@ -194,11 +194,23 @@ def derive_from_bind_info(info: dict | None) -> str | None: 判定依据(字段含义见 `core/douyu/activity_client.py:_parse_bind_info`): - - `is_bound_role` 为真 → **bound**(已经绑了角色,这是"角色绑定"的正主) - - 否则若 `is_bound_act` 为真 → **bound**(活动维度已绑;两个都真才更稳, - 但只给 act 时也算已绑,避免把"已绑但没回角色名"误报成未绑) - - 两个标志都在、且都为假 → **unbound**(明确说了没绑) - - 两个标志**都缺失**(接口没给 / 解析失败)→ **None**(推不出来,别猜) + - `is_bound_role` 为真 → **bound**(已经绑了角色) + - `is_bound_role` 为假 → **unbound**(接口明确说了没绑) + - 缺失(接口没给 / 解析失败)→ **None**(推不出来,别猜) + + ⚠️ **只看 `is_bound_role`,不掺 `is_bound_act`。** + + 这一列问的是「**角色**绑没绑」,而**「活动是否开通/参与」和「是否绑定角色」 + 是两件独立的事**(用户明确指出的领域事实)。生产实测也印证了: + + 电竞 opened=False 且 bind=bound → 187 个账号 + + 也就是说这 187 个账号**绑了角色、但手册压根没开通**。 + 曾经用 `is_bound_act` 做兜底("避免漏掉已绑但没回角色名的"), + 那是把"活动维度"当成了"角色维度" —— 一旦有账号 `is_bound_act=True` + 而角色未绑,就会报出"角色已绑定"的**错结论**。实测没触发过, + 但语义是错的,所以去掉。(当时那些判为 bound 的账号 100% 都有角色名, + 证明 `is_bound_role` 本身就够用。) ⚠️ 这里刻意**不产出 pending**:`pending` 描述的是"正在绑定/待确认"这种 **进行中的动作**(例如刚生成过绑定二维码),那是一次查询的**副作用**, @@ -221,11 +233,9 @@ def derive_from_bind_info(info: dict | None) -> str | None: return None bound_role = flag("is_bound_role") - bound_act = flag("is_bound_act") - if bound_role is True or bound_act is True: + if bound_role is True: return BIND_BOUND - if bound_role is False or bound_act is False: - # 至少有一个标志明确为假 —— 接口给了结论,可以判"未绑定" + if bound_role is False: return BIND_UNBOUND - # 两个标志都缺失:接口没给结论,保持 unknown,别把"没查到"写成"没绑定" + # 接口没给结论:保持 unknown,别把"没查到"写成"没绑定" return None