seaislee1209 c53144b2ac feat(notification): 站内通知系统 — Notification 模型 + 4 个 API + Sidebar 铃铛 + 通知中心页
后端 — 新建 app apps.notifications:
- Notification model:type/title/content/link_url/is_read,索引 (recipient, is_read, -created_at)
- 4 个 endpoint:
  - GET    /api/v1/notifications/         (列表 + 总未读数,unread_only/page/page_size)
  - GET    /api/v1/notifications/unread-count  (轻量,前端 60s 轮询用)
  - PATCH  /api/v1/notifications/<id>/read     (标单条已读)
  - POST   /api/v1/notifications/read-all      (一键全部已读)
- 严格守 user 隔离:所有查询都 filter(recipient=request.user)
- INSTALLED_APPS 注册 + urls.py include
- migration 0001_initial 应用成功
- MySQL 严格模式:所有 CharField 加 default=''(memory feedback_mysql_default)

后端 — anomaly_detector 集成:
- _RULE_LABELS / _team_admin_recipients() / _notify_user_disabled() / _notify_team_disabled() helper
- process_anomalies 里 _disable_user/_disable_team 之后调对应 notify
- 接收人 = 同团队的主管+副管(is_team_admin OR is_team_owner)
- 用 bulk_create 一次写多条
- try/except 保护:通知失败不阻断封禁主流程

前端:
- types/index.ts:AppNotification / NotificationListResponse(避开浏览器 Web API Notification 冲突)
- lib/api.ts:notificationApi (list/getUnreadCount/markRead/markAllRead)
- store/notification.ts:Zustand store 乐观更新(markRead 先动 UI 再发请求)
- pages/NotificationsPage.tsx:标题 + 全部标记已读按钮 + 未读蓝点 + 相对时间 + 点击跳 link_url + 分页
- App.tsx:/notifications 路由(ProtectedRoute 不限 role)
- Sidebar.tsx(用户 76px):铃铛 SVG + 红点 + 60s 轮询 + visibilitychange 立即刷新
- AdminLayout.tsx(超管 220px):同步加铃铛(本来 sub-agent 只加了用户侧 sidebar,我补全 admin 侧)

测试:
- 新建 web/test/v0.20.1-smoke.mjs:11 项 — 铃铛/红点/跳页/标题/100dvh/min-height:0/调试折叠/poster
- 11/11 通过 + v2-smoke 25/25 + modal-interaction 8/8 全部基线 OK
- 后端 4 endpoint 用 curl 验过:list / unread-count / PATCH read / POST read-all 都正常

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:32:29 +08:00

37 lines
1.9 KiB
Python

# Generated by Django 4.2.29 on 2026-05-12 18:24
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Notification',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', models.CharField(choices=[('anomaly_disabled_user', '账号因异常被自动封禁'), ('anomaly_disabled_team', '团队因异常被自动封禁'), ('quota_warning', '额度即将耗尽'), ('system', '系统通知')], default='system', max_length=30, verbose_name='类型')),
('title', models.CharField(blank=True, default='', max_length=200, verbose_name='标题')),
('content', models.TextField(blank=True, default='', verbose_name='内容')),
('link_url', models.CharField(blank=True, default='', max_length=500, verbose_name='跳转链接')),
('is_read', models.BooleanField(db_index=True, default=False, verbose_name='已读')),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')),
('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL, verbose_name='接收人')),
],
options={
'verbose_name': '站内通知',
'verbose_name_plural': '站内通知',
'ordering': ['-created_at'],
'indexes': [models.Index(fields=['recipient', 'is_read', '-created_at'], name='notificatio_recipie_684eac_idx')],
},
),
]