"""P2-03 等级映射 + UserDevice.affinity_level 缓存更新 根据好感度数值映射到对应等级(AffinityLevel),并把结果写回 UserDevice.affinity_level 作为缓存。 设计依据:「好感度系统功能与规则设计.md」§6.3 等级变化规则 - 等级由好感度区间自动映射,每台设备独立判定 - 跨级判定:每次好感度变动后,取当前值所属区间,与上一次等级比较 """ from __future__ import annotations from typing import Optional, Tuple from userapp.models import AffinityLevel def map_value_to_level(value: int) -> Optional[AffinityLevel]: """根据好感度数值找出所属的 AffinityLevel。 匹配规则:min_affinity <= value <= max_affinity 且 is_enabled=True 且 is_deleted=False 返回最高 level 优先(避免重叠区间时的歧义,但 P1 已加 clean 校验拦截重叠)。 若没有匹配到任何区间则返回 None(理论上不应发生,因为等级区间应覆盖 [0, max_affinity])。 """ return ( AffinityLevel.objects .filter( min_affinity__lte=value, max_affinity__gte=value, is_enabled=True, is_deleted=False, ) .order_by('-level') .first() ) def progress_to_next_level(value: int, current_level: AffinityLevel) -> dict: """计算当前值在本等级区间内的进度百分比 + 到下一等级的距离。 返回: { 'percent': 0~100 浮点(当前值在本等级区间内的位置百分比), 'next_level': AffinityLevel 或 None, 'points_to_next': int 或 None, } """ span = max(current_level.max_affinity - current_level.min_affinity, 1) percent = round((value - current_level.min_affinity) / span * 100, 2) next_level = ( AffinityLevel.objects .filter(level__gt=current_level.level, is_enabled=True, is_deleted=False) .order_by('level') .first() ) points_to_next = None if next_level is not None: points_to_next = max(next_level.min_affinity - value, 0) return { 'percent': max(0.0, min(100.0, percent)), 'next_level': next_level, 'points_to_next': points_to_next, } def update_device_level(user_device, save: bool = True) -> Tuple[int, int, Optional[AffinityLevel]]: """根据 user_device.favorability 重新计算并更新 affinity_level 缓存字段。 返回 (old_level, new_level, matched_level_obj) 若没匹配到任何 AffinityLevel,new_level 保持原值,matched_level_obj=None。 save=False 时不调 .save(),由调用方批量保存(service 层)。 """ old_level = user_device.affinity_level matched = map_value_to_level(user_device.favorability) if matched is None: return old_level, old_level, None new_level = matched.level if new_level != old_level: user_device.affinity_level = new_level if save: user_device.save(update_fields=['affinity_level']) return old_level, new_level, matched