后端 — 新建 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>
38 lines
1.5 KiB
Python
38 lines
1.5 KiB
Python
from django.db import models
|
|
from django.conf import settings
|
|
|
|
|
|
class Notification(models.Model):
|
|
"""站内通知 — 异常封禁/额度告警/系统消息等。"""
|
|
|
|
TYPE_CHOICES = [
|
|
('anomaly_disabled_user', '账号因异常被自动封禁'),
|
|
('anomaly_disabled_team', '团队因异常被自动封禁'),
|
|
('quota_warning', '额度即将耗尽'),
|
|
('system', '系统通知'),
|
|
]
|
|
|
|
recipient = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name='notifications',
|
|
verbose_name='接收人',
|
|
)
|
|
type = models.CharField(max_length=30, choices=TYPE_CHOICES, default='system', verbose_name='类型')
|
|
title = models.CharField(max_length=200, blank=True, default='', verbose_name='标题')
|
|
content = models.TextField(blank=True, default='', verbose_name='内容')
|
|
link_url = models.CharField(max_length=500, blank=True, default='', verbose_name='跳转链接')
|
|
is_read = models.BooleanField(default=False, db_index=True, verbose_name='已读')
|
|
created_at = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')
|
|
|
|
class Meta:
|
|
verbose_name = '站内通知'
|
|
verbose_name_plural = '站内通知'
|
|
ordering = ['-created_at']
|
|
indexes = [
|
|
models.Index(fields=['recipient', 'is_read', '-created_at']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f'{self.recipient.username} - {self.title}'
|