video-shuoshan/backend/apps/accounts/migrations/0008_anomaly_detection_phase2.py
seaislee1209 be656900c0
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m13s
feat: v0.9.7 登录风控第二期 — IP归属地解析 + 异常检测(R1-R5) + 飞书告警 + 自动封禁
- IP138 在线 API + ip2region 离线库双通道归属地解析,60 秒熔断降级
- 5 条异常检测规则:地区不对/不可能旅行/频繁登录/团队遍地开花/海外IP太杂
- 飞书 interactive 卡片告警(红色严重/橙色警告),含辅助指标
- R2 自动封禁用户、R4 自动封禁团队,封禁即踢下线
- 系统设置页全局配置 + 团队详情页独立阈值覆盖
- 安全日志页面 + 管理员修改密码入口

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 00:02:56 +08:00

100 lines
5.8 KiB
Python

# Generated by Django 4.2.29 on 2026-03-18 12:11
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('accounts', '0007_set_existing_users_must_change_password_false'),
]
operations = [
migrations.AddField(
model_name='loginrecord',
name='geo_city',
field=models.CharField(blank=True, default='', max_length=50, verbose_name='城市'),
),
migrations.AddField(
model_name='loginrecord',
name='geo_country',
field=models.CharField(blank=True, default='', max_length=50, verbose_name='国家'),
),
migrations.AddField(
model_name='loginrecord',
name='geo_province',
field=models.CharField(blank=True, default='', max_length=50, verbose_name='省份'),
),
migrations.AddField(
model_name='loginrecord',
name='geo_source',
field=models.CharField(blank=True, default='', max_length=10, verbose_name='归属地来源'),
),
migrations.AddField(
model_name='loginrecord',
name='team',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='login_records', to='accounts.team', verbose_name='所属团队'),
),
migrations.AddField(
model_name='team',
name='disabled_by',
field=models.CharField(blank=True, default='', max_length=10, verbose_name='禁用来源'),
),
migrations.AddField(
model_name='team',
name='expected_regions',
field=models.CharField(blank=True, default='', max_length=500, verbose_name='预期登录城市(逗号分隔)'),
),
migrations.AddField(
model_name='user',
name='disabled_by',
field=models.CharField(blank=True, default='', max_length=10, verbose_name='禁用来源'),
),
migrations.CreateModel(
name='TeamAnomalyConfig',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('r1_enabled', models.BooleanField(blank=True, null=True, verbose_name='R1 开关')),
('r2_enabled', models.BooleanField(blank=True, null=True, verbose_name='R2 开关')),
('r2_window_seconds', models.IntegerField(blank=True, null=True, verbose_name='R2 时间窗口(秒)')),
('r3_enabled', models.BooleanField(blank=True, null=True, verbose_name='R3 开关')),
('r3_window_seconds', models.IntegerField(blank=True, null=True, verbose_name='R3 时间窗口(秒)')),
('r3_max_count', models.IntegerField(blank=True, null=True, verbose_name='R3 最大登录次数')),
('r4_enabled', models.BooleanField(blank=True, null=True, verbose_name='R4 开关')),
('r4_window_seconds', models.IntegerField(blank=True, null=True, verbose_name='R4 时间窗口(秒)')),
('r4_city_count', models.IntegerField(blank=True, null=True, verbose_name='R4 预期外城市数阈值')),
('r5_enabled', models.BooleanField(blank=True, null=True, verbose_name='R5 开关')),
('r5_days', models.IntegerField(blank=True, null=True, verbose_name='R5 统计天数')),
('r5_country_count', models.IntegerField(blank=True, null=True, verbose_name='R5 海外国家数阈值')),
('team', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='anomaly_config', to='accounts.team', verbose_name='团队')),
],
options={
'verbose_name': '团队异常检测配置',
'verbose_name_plural': '团队异常检测配置',
},
),
migrations.CreateModel(
name='LoginAnomaly',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('level', models.CharField(choices=[('warning', '警告'), ('critical', '严重')], max_length=10, verbose_name='严重程度')),
('rule', models.CharField(choices=[('region_mismatch', '登录地区不对'), ('impossible_travel', '不可能的旅行'), ('login_frequency', '登录太频繁'), ('multi_city', '团队遍地开花'), ('overseas_ip_diversity', '海外IP太杂')], max_length=30, verbose_name='触发规则')),
('detail', models.JSONField(default=dict, verbose_name='详情')),
('alerted', models.BooleanField(default=False, verbose_name='已发告警')),
('auto_disabled', models.BooleanField(default=False, verbose_name='已自动封禁')),
('disabled_target', models.CharField(blank=True, default='', max_length=10, verbose_name='封禁对象')),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')),
('login_record', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='anomalies', to='accounts.loginrecord', verbose_name='触发登录记录')),
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='login_anomalies', to='accounts.team', verbose_name='团队')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='login_anomalies', to=settings.AUTH_USER_MODEL, verbose_name='用户')),
],
options={
'verbose_name': '登录异常',
'verbose_name_plural': '登录异常',
'ordering': ['-created_at'],
},
),
]