pmc 61e8374e6a fix(affinity-P1): WR-002~WR-009 + IN-001~IN-006 综合改进收尾
WR-002 UserLevelRewardGrant.device on_delete CASCADE → SET_NULL,加 device_snapshot_id,
  unique 改为 partial(device 非空时唯一),与 AffinityLog.device SET_NULL 对齐
WR-003 AffinityLog 删除 3 个低价值索引(user/rule_key/source -created_at 复合)
WR-004 event_id 改为 null=True,partial unique 用 isnull=False;RunPython '' → NULL
WR-005 seed 加 companion_30min 默认规则
WR-006 description 显式 default='';DEFAULT_LEVELS 全部补 description
WR-007 seed_affinity 每条 spec 独立事务,部分失败可重跑
WR-008 ParadiseUser.favorability 字段保留 + UserInfoSerializer 移除暴露 + [DEPRECATED] 标记
WR-009(见 Commit B:AffinityLevel.clean + save full_clean 多层兜底)
IN-001 5 个弃用字段 help_text 加 [DEPRECATED — 计划于 P2 完成后删除]
IN-002 DEFAULT_RULES/LEVELS/SETTING 抽到 userapp/affinity/defaults.py
IN-003 AffinitySetting.daily_cap RenameField → global_daily_cap(区分 AffinityRule.daily_cap)
IN-004 AffinityLog.__str__ 用 pk or 'new' 兜底 None
IN-005(见 Commit A:is_active → is_bound 改名)
IN-006(见 Commit C:0006 print 前缀改为 [migration 0006_...])

迁移 0009 手工修正:daily_cap 改名用 RenameField(保留数据),不是 Remove+Add;
event_id '' → NULL 数据兜底;UserLevelRewardGrant on_delete + conditional unique 重建。

详见 docs/REVIEW-affinity-P1.md WR-* / IN-* 与 FIX-REPORT.md。
2026-05-13 10:18:47 +08:00

119 lines
5.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""P1-10 默认数据 seed 命令
用法:
python manage.py seed_affinity # 写入默认数据,已存在则跳过
python manage.py seed_affinity --force # 强制覆盖已存在的默认规则/等级
写入内容(与「好感度系统功能与规则设计.md」§4.2 / §6.2 一致):
1. AffinitySetting 单例(如不存在)
2. 9 条默认互动规则(含 1 条 companion_time 规则 — WR-005
3. 5 个默认等级
幂等性:
默认按 rule_key规则/ level等级查询已存在则跳过。
--force 模式下覆盖已存在记录的字段(不删旧记录)。
WR-007 修正:
每条 spec 独立事务,部分失败不影响其他记录(避免 force 模式下第 N 条崩
导致前 N-1 条全部回滚但 stdout 已打印 "成功"的语义错位)。
"""
from django.core.management.base import BaseCommand
from django.db import transaction
from userapp.models import AffinityLevel, AffinityRule, AffinitySetting
from userapp.affinity.defaults import DEFAULT_LEVELS, DEFAULT_RULES, DEFAULT_SETTING
class Command(BaseCommand):
help = '初始化好感度系统默认数据AffinitySetting / AffinityRule / AffinityLevel'
def add_arguments(self, parser):
parser.add_argument(
'--force', action='store_true',
help='强制覆盖已存在记录的字段(不删旧记录)',
)
def handle(self, *args, **options):
force = options['force']
# WR-007每条独立事务部分失败可重跑不再用全局 @transaction.atomic
self._seed_setting()
rules_created, rules_updated, rules_failed = self._seed_rules(force)
levels_created, levels_updated, levels_failed = self._seed_levels(force)
style = self.style.WARNING if (rules_failed + levels_failed) else self.style.SUCCESS
self.stdout.write(style(
f'\n[seed_affinity] 完成:'
f'规则 创建 {rules_created} 更新 {rules_updated} 失败 {rules_failed}'
f'等级 创建 {levels_created} 更新 {levels_updated} 失败 {levels_failed}'
))
def _seed_setting(self):
try:
with transaction.atomic():
if AffinitySetting.objects.exists():
self.stdout.write('AffinitySetting 已存在,跳过')
return
# 从 DEFAULT_SETTING 取所有字段(含 IN-003 改名后的 global_daily_cap
AffinitySetting.objects.create(**DEFAULT_SETTING)
self.stdout.write(self.style.SUCCESS('AffinitySetting 已创建(默认值)'))
except Exception as e:
self.stderr.write(self.style.ERROR(f'AffinitySetting 创建失败:{e}'))
def _seed_rules(self, force):
created = 0
updated = 0
failed = 0
for spec in DEFAULT_RULES:
rule_key = spec['rule_key']
try:
with transaction.atomic():
existing = AffinityRule.objects.filter(rule_key=rule_key).first()
if existing is None:
AffinityRule.objects.create(**spec)
created += 1
self.stdout.write(f' + 规则 {rule_key} 已创建')
elif force:
for k, v in spec.items():
setattr(existing, k, v)
existing.save()
updated += 1
self.stdout.write(f' ~ 规则 {rule_key} 已覆盖')
else:
self.stdout.write(f' - 规则 {rule_key} 已存在,跳过')
except Exception as e:
failed += 1
self.stderr.write(self.style.ERROR(f' ! 规则 {rule_key} 处理失败:{e}'))
continue
return created, updated, failed
def _seed_levels(self, force):
created = 0
updated = 0
failed = 0
for spec in DEFAULT_LEVELS:
level_num = spec['level']
try:
with transaction.atomic():
existing = AffinityLevel.objects.filter(level=level_num).first()
if existing is None:
# 用 skip_clean=True 跳过 save 内 full_clean区间重叠校验依赖已存在 levels
# 但当前批量 seed 是按 level 升序逐条 commit跨记录关系会随插入逐步成立
# 这里仍执行 clean 以保证区间合法
AffinityLevel.objects.create(**spec)
created += 1
self.stdout.write(f' + 等级 Lv{level_num} 已创建')
elif force:
for k, v in spec.items():
setattr(existing, k, v)
existing.save()
updated += 1
self.stdout.write(f' ~ 等级 Lv{level_num} 已覆盖')
else:
self.stdout.write(f' - 等级 Lv{level_num} 已存在,跳过')
except Exception as e:
failed += 1
self.stderr.write(self.style.ERROR(f' ! 等级 Lv{level_num} 处理失败:{e}'))
continue
return created, updated, failed