fix(binding): 角色绑定只看 is_bound_role —— 「是否开通」与「是否绑定角色」是两件事

用户明确指出:**精英/电竞是否开通,和是否绑定角色没关系**。

## 我确实混了(虽然这次没触发)

`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 全绿。
This commit is contained in:
yml2213
2026-10-04 21:01:42 +08:00
parent ccd05cba9c
commit 8f231e6edf
2 changed files with 48 additions and 15 deletions
+28 -5
View File
@@ -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。