"""P2 admin 接口序列化器 为 admin 接口提供 AffinityRule / AffinityLevel / AffinitySetting / AffinityLog 等的 读写序列化器。读字段尽量完整(含 deprecated 字段以便审计),写字段排除 deprecated。 所有 ModelSerializer 都用 PATCH 友好的 partial-update 模式。 """ from __future__ import annotations from rest_framework import serializers from device_interaction.models import UserDevice from userapp.models import ( AffinityLevel, AffinityLog, AffinityRule, AffinitySetting, ) # ---------- P2-06 AffinityRule ---------- class AffinityRuleSerializer(serializers.ModelSerializer): class Meta: model = AffinityRule fields = [ 'id', 'rule_key', 'name', 'description', 'trigger_type', 'min_change', 'max_change', 'single_cap', 'daily_cap', 'cooldown_seconds', 'is_negative', 'is_enabled', 'is_deleted', 'min_continuous_minutes', 'max_count_per_day', 'created_at', 'updated_at', ] read_only_fields = ['id', 'created_at', 'updated_at'] def validate_rule_key(self, value): """rule_key 唯一性校验(数据库 unique=True 已保证,但 admin 写入时显式拦截更友好)""" if value is None or value == '': raise serializers.ValidationError('rule_key 不能为空') # 排除当前对象自身(编辑场景) qs = AffinityRule.objects.filter(rule_key=value) if self.instance is not None: qs = qs.exclude(id=self.instance.id) if qs.exists(): raise serializers.ValidationError(f'rule_key "{value}" 已存在') return value def validate(self, attrs): """业务级跨字段校验(P1 CHECK 是最终防线,这里给 admin 友好错误)""" instance = self.instance get = lambda k, d=None: attrs.get(k, getattr(instance, k, d) if instance else d) min_change = get('min_change', 1) max_change = get('max_change', 1) if min_change is not None and max_change is not None and min_change > max_change: raise serializers.ValidationError({'max_change': 'max_change 必须 >= min_change'}) single_cap = get('single_cap', 10) daily_cap = get('daily_cap', 20) if single_cap is not None and single_cap <= 0: raise serializers.ValidationError({'single_cap': 'single_cap 必须 > 0'}) if daily_cap is not None and daily_cap <= 0: raise serializers.ValidationError({'daily_cap': 'daily_cap 必须 > 0'}) cooldown = get('cooldown_seconds', 0) if cooldown is not None and cooldown < 0: raise serializers.ValidationError({'cooldown_seconds': 'cooldown_seconds 不能小于 0'}) trigger_type = get('trigger_type', 'action') if trigger_type == 'companion_time': mcm = get('min_continuous_minutes') mcp = get('max_count_per_day') if not mcm or mcm <= 0: raise serializers.ValidationError({ 'min_continuous_minutes': '陪伴时长规则必须设置 min_continuous_minutes > 0' }) if not mcp or mcp <= 0: raise serializers.ValidationError({ 'max_count_per_day': '陪伴时长规则必须设置 max_count_per_day > 0' }) return attrs # ---------- P2-07 AffinityLevel ---------- class AffinityLevelSerializer(serializers.ModelSerializer): class Meta: model = AffinityLevel fields = [ 'id', 'level', 'name', 'description', 'min_affinity', 'max_affinity', 'unlock_content', 'reward_type', 'reward_currency', 'reward_items', 'is_enabled', 'is_deleted', 'created_at', 'updated_at', ] read_only_fields = ['id', 'created_at', 'updated_at'] def validate(self, attrs): """区间合法 + 不与其他等级重叠(P1 已有 model.clean() 兜底,这里前置给友好错误)""" instance = self.instance get = lambda k, d=None: attrs.get(k, getattr(instance, k, d) if instance else d) min_a = get('min_affinity', 0) max_a = get('max_affinity', 0) if min_a is None or max_a is None: raise serializers.ValidationError('min_affinity / max_affinity 不能为空') if min_a > max_a: raise serializers.ValidationError({'max_affinity': 'max_affinity 必须 >= min_affinity'}) # 检查与其他启用等级的区间是否重叠 level_num = get('level') qs = AffinityLevel.objects.filter( is_enabled=True, is_deleted=False, min_affinity__lte=max_a, max_affinity__gte=min_a, ) if instance is not None: qs = qs.exclude(id=instance.id) if level_num is not None: qs = qs.exclude(level=level_num) conflict = qs.first() if conflict: raise serializers.ValidationError({ 'min_affinity': f'区间 [{min_a}, {max_a}] 与 Lv{conflict.level} ' f'[{conflict.min_affinity}, {conflict.max_affinity}] 重叠' }) return attrs # ---------- P2-08 AffinitySetting ---------- class AffinitySettingSerializer(serializers.ModelSerializer): class Meta: model = AffinitySetting fields = [ 'id', 'initial_affinity', 'max_affinity', 'global_daily_cap', 'decay_rate', 'decay_threshold', 'decay_min_decay', 'decay_max_decay', 'decay_cap', 'decay_min_floor', 'enable_notify', 'enable_rewards', 'notify_decay', 'timezone', 'created_at', 'updated_at', ] read_only_fields = ['id', 'created_at', 'updated_at'] def validate(self, attrs): instance = self.instance get = lambda k, d=None: attrs.get(k, getattr(instance, k, d) if instance else d) init = get('initial_affinity', 10) mx = get('max_affinity', 100) if init > mx: raise serializers.ValidationError({ 'initial_affinity': 'initial_affinity 不能超过 max_affinity' }) dec_min = get('decay_min_decay', 1) dec_max = get('decay_max_decay', 3) dec_cap = get('decay_cap', 5) if dec_min > dec_max: raise serializers.ValidationError({ 'decay_max_decay': 'decay_max_decay 必须 >= decay_min_decay' }) if dec_max > dec_cap: raise serializers.ValidationError({ 'decay_cap': 'decay_cap 必须 >= decay_max_decay' }) gcap = get('global_daily_cap', 20) if gcap <= 0: raise serializers.ValidationError({ 'global_daily_cap': 'global_daily_cap 必须 > 0' }) return attrs # ---------- P2-09 AffinityLog 查询(read-only)---------- class AffinityLogSerializer(serializers.ModelSerializer): user_username = serializers.CharField(source='user.username', read_only=True) device_code = serializers.CharField( source='device.device.device_code', read_only=True, default=None, ) rule_name = serializers.CharField(source='rule.name', read_only=True, default=None) class Meta: model = AffinityLog fields = [ 'id', 'user', 'user_username', 'device', 'device_code', 'rule', 'rule_key', 'rule_name', 'change_value', 'before_value', 'after_value', 'source', 'event_id', 'operator_admin_id', 'reason', 'metadata', 'created_at', ] read_only_fields = fields # 全部只读 # ---------- P2-11 UserDevice 好感度展开(read-only)---------- class UserDeviceAffinitySerializer(serializers.ModelSerializer): """admin 按 user 展开该用户名下所有设备的好感度状态""" device_code = serializers.CharField(source='device.device_code', read_only=True) mac_address = serializers.CharField(source='device.mac_address', read_only=True) device_status = serializers.CharField(source='device.status', read_only=True) user_username = serializers.CharField(source='user.username', read_only=True) level_name = serializers.SerializerMethodField() class Meta: model = UserDevice fields = [ 'id', 'user', 'user_username', 'device', 'device_code', 'mac_address', 'device_status', 'nickname', 'bound_at', 'is_primary', 'is_bound', 'favorability', 'affinity_level', 'level_name', 'last_active_at', ] read_only_fields = fields def get_level_name(self, obj): # 取等级名(避免 N+1,调用方应在 viewset 里 prefetch_related 或单次缓存) level = AffinityLevel.objects.filter(level=obj.affinity_level).first() return level.name if level else None # ---------- P2-12 adjust 请求 ---------- class AffinityAdjustSerializer(serializers.Serializer): """单台设备调整:必传 device_id + delta""" user_id = serializers.IntegerField(required=True, help_text='目标用户 ID') device_id = serializers.IntegerField(required=True, help_text='UserDevice 绑定 ID') delta = serializers.IntegerField(required=True, help_text='增减值,正数加,负数减') reason = serializers.CharField(required=False, allow_blank=True, default='') def validate_delta(self, value): if value == 0: raise serializers.ValidationError('delta 不能为 0') return value class AffinityAdjustBatchSerializer(serializers.Serializer): """批量调整:给某 user 名下所有绑定设备各加 delta""" user_id = serializers.IntegerField(required=True) delta = serializers.IntegerField(required=True) reason = serializers.CharField(required=False, allow_blank=True, default='') def validate_delta(self, value): if value == 0: raise serializers.ValidationError('delta 不能为 0') return value