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。
119 lines
5.3 KiB
Python
119 lines
5.3 KiB
Python
"""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
|